IT박스

파일 시스템 종속성이있는 단위 테스트 코드

itboxs 2020. 7. 2. 08:05
반응형

파일 시스템 종속성이있는 단위 테스트 코드


ZIP 파일이 주어지면 다음을 수행 해야하는 구성 요소를 작성 중입니다.

  1. 파일을 압축 해제하십시오.
  2. 압축이 풀린 파일 중에서 특정 dll을 찾으십시오.
  3. 리플렉션을 통해 해당 dll을로드하고 그것에 대한 메소드를 호출하십시오.

이 구성 요소를 단위 테스트하고 싶습니다.

파일 시스템을 직접 다루는 코드를 작성하려고합니다.

void DoIt()
{
   Zip.Unzip(theZipFile, "C:\\foo\\Unzipped");
   System.IO.File myDll = File.Open("C:\\foo\\Unzipped\\SuperSecret.bar");
   myDll.InvokeSomeSpecialMethod();
}

그러나 사람들은 종종 "파일 시스템, 데이터베이스, 네트워크 등에 의존하는 단위 테스트를 작성하지 마십시오"라고 말합니다.

이것을 단위 테스트 친화적 인 방식으로 작성한다면 다음과 같이 보일 것입니다.

void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner)
{
   string path = zipper.Unzip(theZipFile);
   IFakeFile file = fileSystem.Open(path);
   runner.Run(file);
}

예이! 이제 테스트 할 수 있습니다. DoIt 방법에 테스트 복식 (모의)을 넣을 수 있습니다. 그러나 어떤 비용으로? 나는 이것을 테스트 할 수 있도록 3 개의 새로운 인터페이스를 정의해야했습니다. 그리고 정확히 무엇을 테스트하고 있습니까? DoIt 기능이 종속성과 올바르게 상호 작용하는지 테스트 중입니다. zip 파일이 제대로 압축 해제되었는지 테스트하지 않습니다.

더 이상 기능을 테스트하는 것 같지 않습니다. 수업 상호 작용을 테스트하는 것 같습니다.

내 질문은 이것입니다 : 파일 시스템에 의존하는 것을 단위 테스트하는 적절한 방법은 무엇입니까?

편집 .NET을 사용하고 있지만이 개념은 Java 또는 기본 코드를 적용 할 수도 있습니다.


이것에는 아무런 문제가 없습니다. 단위 테스트인지 통합 테스트인지에 대한 질문입니다. 파일 시스템과 상호 작용하는 경우 의도하지 않은 부작용이 없는지 확인하면됩니다. 특히, 자신을 정리 한 후 생성 한 임시 파일을 삭제하고 사용중인 임시 파일과 동일한 파일 이름을 가진 기존 파일을 실수로 덮어 쓰지 않아야합니다. 항상 절대 경로가 아닌 상대 경로를 사용하십시오.

또한 chdir()테스트를 실행하기 전에 임시 디렉토리로 이동 한 chdir()후 다시 돌아가는 것이 좋습니다 .


예이! 이제 테스트 할 수 있습니다. DoIt 방법에 테스트 복식 (모의)을 넣을 수 있습니다. 그러나 어떤 비용으로? 나는 이것을 테스트 할 수 있도록 3 개의 새로운 인터페이스를 정의해야했습니다. 그리고 정확히 무엇을 테스트하고 있습니까? DoIt 기능이 종속성과 올바르게 상호 작용하는지 테스트 중입니다. zip 파일이 제대로 압축 해제되었는지 테스트하지 않습니다.

머리에 손톱이 맞았습니다. 테스트하려는 것은 실제 파일을 처리 할 수 ​​있는지 여부가 아니라 메소드의 논리입니다. 파일이 올바르게 압축 해제되었는지 여부를 테스트 할 필요는 없습니다.이 방법은 당연합니다. 인터페이스는 하나의 구체적인 구현에 암시 적 또는 명시 적으로 의존하지 않고 프로그래밍 할 수있는 추상화를 제공하기 때문에 그 자체로 가치가 있습니다.


귀하의 질문은 개발자가 테스트에 들어가기 가장 어려운 테스트 중 하나를 노출시킵니다.

"도대체 내가 무엇을 테스트해야합니까?"

예제는 API 호출을 서로 붙이기 때문에 매우 흥미롭지 않으므로 단위 테스트를 작성하려면 메서드가 호출되었다는 단언이됩니다. 이와 같은 테스트는 구현 세부 사항을 테스트에 밀접하게 연결합니다. 구현 세부 사항을 변경하면 테스트가 중단되므로 메소드의 구현 세부 사항을 변경할 때마다 테스트를 변경해야하기 때문에 이것은 나쁩니다!

나쁜 테스트를하는 것은 실제로 테스트를하지 않는 것보다 나쁩니다.

귀하의 예에서 :

void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner)
{
   string path = zipper.Unzip(theZipFile);
   IFakeFile file = fileSystem.Open(path);
   runner.Run(file);
}

모의를 전달할 수는 있지만 테스트 방법에는 논리가 없습니다. 이를 위해 단위 테스트를 시도하면 다음과 같이 보일 수 있습니다.

// Assuming that zipper, fileSystem, and runner are mocks
void testDoIt()
{
  // mock behavior of the mock objects
  when(zipper.Unzip(any(File.class)).thenReturn("some path");
  when(fileSystem.Open("some path")).thenReturn(mock(IFakeFile.class));

  // run the test
  someObject.DoIt(zipper, fileSystem, runner);

  // verify things were called
  verify(zipper).Unzip(any(File.class));
  verify(fileSystem).Open("some path"));
  verify(runner).Run(file);
}

Congratulations, you basically copy-pasted the implementation details of your DoIt() method into a test. Happy maintaining.

When you write tests you want to test the WHAT and not the HOW. See Black Box Testing for more.

The WHAT is the name of your method (or at least it should be). The HOW are all the little implementation details that live inside your method. Good tests allow you to swap out the HOW without breaking the WHAT.

Think about it this way, ask yourself:

"If I change the implementation details of this method (without altering the public contract) will it break my test(s)?"

If the answer is yes, you are testing the HOW and not the WHAT.

To answer your specific question about testing code with file system dependencies, let's say you had something a bit more interesting going on with a file and you wanted to save the Base64 encoded contents of a byte[] to a file. You can use streams for this to test that your code does the right thing without having to check how it does it. One example might be something like this (in Java):

interface StreamFactory {
    OutputStream outStream();
    InputStream inStream();
}

class Base64FileWriter {
    public void write(byte[] contents, StreamFactory streamFactory) {
        OutputStream outputStream = streamFactory.outStream();
        outputStream.write(Base64.encodeBase64(contents));
    }
}

@Test
public void save_shouldBase64EncodeContents() {
    OutputStream outputStream = new ByteArrayOutputStream();
    StreamFactory streamFactory = mock(StreamFactory.class);
    when(streamFactory.outStream()).thenReturn(outputStream);

    // Run the method under test
    Base64FileWriter fileWriter = new Base64FileWriter();
    fileWriter.write("Man".getBytes(), streamFactory);

    // Assert we saved the base64 encoded contents
    assertThat(outputStream.toString()).isEqualTo("TWFu");
}

The test uses a ByteArrayOutputStream but in the application (using dependency injection) the real StreamFactory (perhaps called FileStreamFactory) would return FileOutputStream from outputStream() and would write to a File.

What was interesting about the write method here is that it was writing the contents out Base64 encoded, so that's what we tested for. For your DoIt() method, this would be more appropriately tested with an integration test.


I am reticent to pollute my code with types and concepts that exist only to facilitate unit testing. Sure, if it makes the design cleaner and better then great, but I think that is often not the case.

My take on this is that your unit tests would do as much as they can which may not be 100% coverage. In fact, it may only be 10%. The point is, your unit tests should be fast and have no external dependencies. They might test cases like "this method throws an ArgumentNullException when you pass in null for this parameter".

I would then add integration tests (also automated and probably using the same unit testing framework) that can have external dependencies and test end-to-end scenarios such as these.

When measuring code coverage, I measure both unit and integration tests.


There's nothing wrong with hitting the file system, just consider it an integration test rather than a unit test. I'd swap the hard coded path with a relative path and create a TestData subfolder to contain the zips for the unit tests.

If your integration tests take too long to run then separate them out so they aren't running as often as your quick unit tests.

I agree, sometimes I think interaction based testing can cause too much coupling and often ends up not providing enough value. You really want to test unzipping the file here not just verify you are calling the right methods.


One way would be to write the unzip method to take InputStreams. Then the unit test could construct such an InputStream from a byte array using ByteArrayInputStream. The contents of that byte array could be a constant in the unit test code.


This seems to be more of an integration test as you are depending on a specific detail (the file system) that could change, in theory.

I would abstract the code that deals with the OS into it's own module (class, assembly, jar, whatever). In your case you want to load a specific DLL if found, so make an IDllLoader interface and DllLoader class. Have your app acquire the DLL from the DllLoader using the interface and test that .. you're not responsible for the unzip code afterall right?


Assuming that "file system interactions" are well tested in the framework itself, create your method to work with streams, and test this. Opening a FileStream and passing it to the method can be left out of your tests, as FileStream.Open is well tested by the framework creators.


You should not test class interaction and function calling. instead you should consider integration testing. Test the required result and not the file loading operation.


For unit test I would suggest that you include the test file in your project(EAR file or equivalent) then use a relative path in the unit tests i.e. "../testdata/testfile".

As long as your project is correctly exported/imported than your unit test should work.


As others have said, the first is fine as an integration test. The second tests only what the function is supposed to actually do, which is all a unit test should do.

As shown, the second example looks a little pointless, but it does give you the opportunity to test how the function responds to errors in any of the steps. You don't have any error checking in the example, but in the real system you may have, and the dependency injection would let you test all the responses to any errors. Then the cost will have been worth it.

참고URL : https://stackoverflow.com/questions/129036/unit-testing-code-with-a-file-system-dependency

반응형