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


138

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 또는 기본 코드를 적용 할 수도 있습니다.


8
사람들은 단위 테스트에서 파일 시스템에 쓰지 않는다고 말합니다. 파일 시스템에 쓰려고한다면 단위 테스트가 무엇인지 이해하지 못하기 때문입니다. 단위 테스트는 일반적으로 단일 실제 객체 (테스트중인 단위)와 상호 작용 하고 다른 모든 종속성은 조롱 및 전달됩니다. 그런 다음 테스트 클래스는 객체의 메소드를 통한 논리 경로와 논리 경로의 유효성을 검사하는 테스트 메소드로 구성됩니다. 테스트중인 장치.
Christopher Perry

1
귀하의 상황에서 단위 테스트가 필요한 유일한 부분은 myDll.InvokeSomeSpecialMethod();성공 및 실패 상황 모두에서 올바르게 작동하는지 확인하는 것이므로 단위 테스트는 DoIt아니지만 DllRunner.RunUNIT 테스트를 잘못 사용하여 전체 프로세스가 작동하는지 두 번 확인했습니다. 허용되는 오용과 단위 테스트를 가장 한 통합 테스트이므로 일반적인 단위 테스트 규칙을 엄격하게 적용 할 필요는 없습니다.
MikeT

답변:


47

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

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


27
그러나 +1이지만 chdir()프로세스 전체에 적용되므로 테스트 프레임 워크 또는 향후 버전에서 지원하는 경우 테스트를 병렬로 실행할 수 없습니다.

69

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

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


12
DoIt명시된 테스트 가능 기능은 테스트가 필요하지 않습니다. 질문자가 올바르게 지적했듯이 테스트 할 의미는 없습니다. 이제의 구현입니다 IZipper, IFileSystem그리고 IDllRunner요구 사항이 테스트하지만 테스트를 위해 밖으로 조롱 한 바로 그 일 것을!
Ian Goldby

56

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

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

예제는 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);
}

축하합니다. 기본적으로 DoIt()메소드 의 구현 세부 사항을 테스트에 복사하여 붙여 넣었습니다 . 행복한 유지.

테스트를 작성할 때 HOW가 아닌 WHAT 를 테스트하려고합니다 . 자세한 내용은 블랙 박스 테스트 를 참조하십시오 .

무엇 당신의 방법의 이름 (또는 적어도 그것은이어야 함). 는 어떻게 모든 방법 안에 살고있는 작은 구현 세부 사항입니다. 좋은 테스트를 통해 WHAT 를 중단하지 않고 HOW 를 교체 할 수 있습니다 .

이런 식으로 생각하고 스스로에게 물어보십시오.

"공개 계약을 변경하지 않고이 방법의 구현 세부 사항을 변경하면 테스트가 중단됩니까?"

대답이 예이면 WHAT가 아니라 HOW를 테스트하는 것 입니다.

파일 시스템 종속성을 사용하여 코드를 테스트하는 것에 대한 특정 질문에 대답하기 위해 파일에 대해 좀 더 흥미로운 내용이 있고 Base64로 인코딩 된 내용을 byte[]파일 에 저장하려고한다고 가정 해 봅시다 . 이를 위해 스트림을 사용하여 코드의 작동 방식 을 확인하지 않고도 코드가 올바르게 작동하는지 테스트 할 수 있습니다. 한 가지 예는 다음과 같습니다 (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");
}

테스트는 사용 ByteArrayOutputStream되지만 응용 프로그램에 반환 (아마도 FileStreamFactory라고 함) 실제 StreamFactory (의존성 주입을 사용) FileOutputStream에서 outputStream()하고 쓸 것이다 File.

write방법에서 흥미로운 점은 Base64로 인코딩 된 내용을 작성하고 있다는 것입니다. 그래서 우리가 테스트 한 것입니다. 귀하의 경우 DoIt()방법이 더 적절하게 테스트 할 것입니다 통합 테스트 .


1
나는 당신의 메시지에 동의하지 않습니다. 이런 종류의 방법을 단위 테스트 할 필요가 없다고 말하는가? 기본적으로 TDD가 잘못되었다고 말하는 것입니까? TDD를 수행하는 것처럼 테스트를 먼저 작성하지 않으면이 메소드를 작성할 수 없습니다. 또는 분석법에 테스트가 필요하지 않다는 직감에 의존하고 있습니까? 모든 단위 테스트 프레임 워크에 "확인"기능이 포함 된 이유는 사용하는 것이 좋습니다. "이 방법은 구현의 세부 사항을 변경할 때마다 테스트를 변경해야하기 때문에 좋지 않습니다."단위 테스트의 세계에 오신 것을 환영합니다.
Ronnie

2
메소드가 아닌 구현의 CONTRACT를 테스트해야합니다. 계약의 구현이 변경 될 때마다 테스트를 변경해야하는 경우 앱 코드베이스와 테스트 코드베이스를 모두 유지하는 끔찍한 시간입니다.
Christopher Perry

@Ronnie는 맹목적으로 단위 테스트를 적용하는 것은 도움이되지 않습니다. 매우 다양한 성격의 프로젝트가 있으며 단위 테스트는 그다지 효과적이지 않습니다. 예를 들어, 내가 코드의 95 % 인 프로젝트에서 일하고 있어요 에 대한 부작용 (주이 부작용 무거운 성격은 요구 사항에 의해 그것의, 필수 복잡성, 부수적하지 가 데이터를 수집하기 때문에, 다양한 상태 저장 소스를 제공하며 조작이 거의 없으므로 순수한 논리는 거의 없습니다. 여기서 단위 테스트는 효과적 이지 않으며 통합 테스트는 효과적입니다.
Vicky Chijwani 2012

부작용은 시스템 가장자리로 밀려 나야하며 레이어 전체에 얽 히지 않아야합니다. 가장자리에서 부작용 인 부작용을 테스트합니다. 다른 곳에서는 부작용없이 순수한 기능을 갖추려고 노력해야합니다. 부작용없이 쉽게 테스트하고 추론하고 재사용하고 구성 할 수 있습니다.
Christopher Perry

24

단위 테스트를 용이하게하기 위해 존재하는 유형과 개념으로 코드를 오염시키는 것을 원합니다. 물론, 디자인을 더 깨끗하고 훌륭하게 만들면 종종 그렇지 않습니다.

이것에 대한 나의 취지는 100 % 적용 범위가 아닌 단위 테스트가 가능한 한 많이 수행한다는 것입니다. 실제로 10 %에 불과할 수 있습니다. 요점은, 단위 테스트가 빠르며 외부 종속성이 없어야한다는 것입니다. "이 매개 변수에 대해 null을 전달하면이 메서드에서 ArgumentNullException이 발생합니다"와 같은 사례를 테스트 할 수 있습니다.

그런 다음 외부 종속성이 있고 이러한 엔드 투 엔드 시나리오를 테스트 할 수있는 통합 테스트 (자동화 및 아마도 동일한 단위 테스트 프레임 워크 사용)를 추가합니다.

코드 범위를 측정 할 때 단위 테스트와 통합 테스트를 모두 측정합니다.


5
그래, 들었어 당신이 너무 많이 분리 한 곳에 도달하는이 기괴한 세계가 있습니다. 당신의 모든 왼쪽은 추상 객체에 대한 메소드 호출입니다. 바람이 잘 통하는 보풀. 이 시점에 도달하면 실제로 실제 테스트하는 느낌이 들지 않습니다. 당신은 클래스 간의 상호 작용을 테스트하고 있습니다.
유다 가브리엘 히 망고

6
이 답변은 잘못된 것입니다. 단위 테스트는 설탕 프로스팅과 같지 않으며 설탕과 비슷합니다. 케이크에 구운 것입니다. 코드 작성의 일부입니다. 디자인 활동입니다. 따라서 테스트는 코드 작성을 용이하게하기 때문에 "테스트를 용이하게하는"것으로 코드를 "폴링"하지 마십시오. 개발자가 테스트 전에 코드를 작성하고 테스트 할 수없는 사악한
Christopher Perry

1
@Christopher : 당신의 비유를 확장하기 위해 케이크가 바닐라 슬라이스와 닮아서 설탕을 사용할 수 있기를 원하지 않습니다. 내가 옹호하는 것은 실용주의입니다.
Kent Boogaart

1
@Christopher : 당신의 바이오는 모든 것을 말합니다 : "나는 TDD 열광 자입니다." 반면에 나는 실용적입니다. 나는 그것이 맞는 곳이 아닌 곳에서 TDD를합니다. 내 대답에 아무것도 TDD를하지 않는다고 제안하지만 아무것도 생각하지 않습니다. 그리고 TDD이든 아니든, 테스트를 용이하게하기 위해 많은 복잡성을 도입하지는 않을 것입니다.
Kent Boogaart

3
@ChristopherPerry OP의 원래 문제를 TDD 방식으로 해결하는 방법을 설명 할 수 있습니까? 나는 항상 이것에 부딪칩니다. 이 질문 에서처럼 외부 의존성을 가진 작업을 수행하는 것이 유일한 목적인 함수를 작성해야합니다. 따라서 테스트 후 쓰기 시나리오에서도 그 테스트는 무엇일까요?
Dax Fohl

8

파일 시스템에 타격을 가하는 것은 아무 문제가 없습니다. 단위 테스트가 아닌 통합 테스트를 고려하십시오. 하드 코딩 된 경로를 상대 경로로 바꾸고 단위 테스트를위한 zip을 포함하는 TestData 하위 폴더를 만듭니다.

통합 테스트를 실행하는 데 시간이 너무 오래 걸리면 빠른 단위 테스트만큼 자주 실행되지 않도록 분리하십시오.

때로는 상호 작용 기반 테스트가 너무 많은 커플 링을 유발하고 종종 충분한 가치를 제공하지 못하는 것으로 생각합니다. 올바른 메소드를 호출하고 있는지 확인하는 것이 아니라 파일 압축 풀기를 테스트하고 싶습니다.


그들이 얼마나 자주 달려가는지는 거의 관심이 없다. 우리는 자동 통합 서버를 사용합니다. 우리는 그들이 얼마나 오래 걸리는지 상관하지 않습니다. "실행 시간"이 중요하지 않은 경우, 단위 테스트와 통합 테스트를 구별 할 이유가 있습니까?
유다 가브리엘 히 망고

4
실제로는 아닙니다. 그러나 개발자가 모든 단위 테스트를 로컬에서 빠르게 실행하려면 쉽게 할 수있는 좋은 방법이 있습니다.
JC.

6

한 가지 방법은 unzip 메소드를 작성하여 InputStream을 가져 오는 것입니다. 그런 다음 단위 테스트는 ByteArrayInputStream을 사용하여 바이트 배열에서 이러한 InputStream을 구성 할 수 있습니다. 해당 바이트 배열의 내용은 단위 테스트 코드에서 상수 일 수 있습니다.


그래야 스트림 주입이 가능합니다. 의존성 주입 / IOC. 스트림을 파일로 압축 해제하고 해당 파일 중 dll을로드하고 해당 dll에서 메소드를 호출하는 부분은 어떻습니까?
유다 가브리엘 히 망고 2009 년

3

이론적으로 변경 될 수있는 특정 세부 사항 (파일 시스템)에 따라 통합 테스트에 더 가깝게 보입니다.

OS를 다루는 코드를 자체 모듈 (클래스, 어셈블리, jar 등)로 추상화합니다. 귀하의 경우 특정 DLL을 찾은 경우로드하려면 IDllLoader 인터페이스와 DllLoader 클래스를 만드십시오. 앱이 인터페이스를 사용하여 DllLoader에서 DLL을 가져 와서 테스트했는지 확인하십시오.


2

"파일 시스템 상호 작용"이 프레임 워크 자체에서 잘 테스트되었다고 가정하고 스트림 작업 방법을 작성하고이를 테스트하십시오. FileStream.Open은 프레임 워크 작성자가 잘 테스트하므로 FileStream을 열고 메소드에 전달하는 것은 테스트에서 제외 될 수 있습니다.


당신과 nsayer는 본질적으로 동일한 제안을 가지고 있습니다 : 내 코드를 스트림과 함께 작동하게하십시오. 스트림 내용을 dll 파일로 압축 해제하고 dll을 열고 함수를 호출하는 방법은 어떻습니까? 거기서 무엇을 하시겠습니까?
유다 가브리엘 히 망고

3
@JudahHimango. 이러한 부분은 반드시 테스트 할 필요는 없습니다. 모든 것을 테스트 할 수는 없습니다. 테스트 할 수없는 구성 요소를 자체 기능 블록으로 추상화하고 작동한다고 가정합니다. 이 블록이 작동하는 방식으로 버그를 발견하면 테스트를 해보십시오. 단위 테스트가 모든 것을 테스트해야한다는 의미는 아닙니다. 일부 시나리오에서는 100 % 코드 적용 범위가 비현실적입니다.
Zoran Pavlovic

1

클래스 상호 작용 및 함수 호출을 테스트해서는 안됩니다. 대신 통합 테스트를 고려해야합니다. 파일로드 작업이 아닌 필요한 결과를 테스트하십시오.


1

단위 테스트의 경우 프로젝트에 테스트 파일 (EAR 파일 또는 이와 동등한 파일)을 포함시킨 다음 단위 테스트에서 상대 경로 (예 : "../testdata/testfile")를 사용하는 것이 좋습니다.

단위 테스트가 작동하는 것보다 프로젝트를 올바르게 내보내거나 가져 오는 한.


0

다른 사람들이 말했듯이 첫 번째는 통합 테스트로 괜찮습니다. 두 번째는 함수가 실제로 수행해야하는 것만 테스트합니다. 이는 모두 단위 테스트입니다.

그림과 같이 두 번째 예는 약간 의미가 없지만 어떤 단계에서든 함수가 오류에 어떻게 반응하는지 테스트 할 수있는 기회를 제공합니다. 예제에는 오류 검사가 없지만 실제 시스템에는 오류 검사가 있으며 종속성 주입을 사용하면 오류에 대한 모든 응답을 테스트 할 수 있습니다. 그렇다면 그 비용은 가치가있을 것입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.