2010-02-12 2 views
6

이전에 다른 이름으로이 질문을했지만 잘 설명하지 않았으므로 삭제했습니다.유닛 테스트 자원 관리 클래스의 개인 메소드

파일을 관리하는 클래스가 있다고 가정 해 보겠습니다. 의이 클래스는 특정 파일 형식을 필요로 파일을 처리하고,이 파일에 대한 작업을 수행하는 방법을 포함한다고 가정 해 봅시다 :

class Foo { 
    std::wstring fileName_; 
public: 
    Foo(const std::wstring& fileName) : fileName_(fileName) 
    { 
     //Construct a Foo here. 
    }; 
    int getChecksum() 
    { 
     //Open the file and read some part of it 

     //Long method to figure out what checksum it is. 

     //Return the checksum. 
    } 
}; 

하는의 내가 단위 테스트에이 클래스의 일부 수 있도록하고 싶습니다 가정 해 봅시다을 그 체크섬을 계산합니다. 단위 테스트는 파일에로드하는 클래스의 부분을 테스트하는 것이기 때문에 getChecksum() 메소드의 모든 부분을 테스트하기 때문에 40 또는 50 개의 파일을 작성해야 할 수도 있습니다.

이제 클래스의 다른 위치에서 체크섬 방법을 다시 사용하고 싶습니다. 그것을 테스트하기 쉽고 복잡하기 때문에

이제
class Foo { 
    std::wstring fileName_; 
    static int calculateChecksum(const std::vector<unsigned char> &fileBytes) 
    { 
     //Long method to figure out what checksum it is. 
    } 
public: 
    Foo(const std::wstring& fileName) : fileName_(fileName) 
    { 
     //Construct a Foo here. 
    }; 
    int getChecksum() 
    { 
     //Open the file and read some part of it 

     return calculateChecksum(something); 
    } 
    void modifyThisFileSomehow() 
    { 
     //Perform modification 

     int newChecksum = calculateChecksum(something); 

     //Apply the newChecksum to the file 
    } 
}; 

내가 유닛 calculateChecksum() 방법을 테스트하고 싶습니다, 그리고 그것을이기 때문에 단위 테스트 getChecksum() 걱정하지 않는다 : 지금과 같이 보이도록 내가 방법을 추출 간단하고 테스트하기가 매우 어렵습니다. 그러나 이기 때문에 calculateChecksum()을 직접 테스트 할 수 없습니다.

누구든지이 문제의 해결책을 알고 있습니까?

답변

2

기본적으로 mock 단위 테스트가 더 적합하도록하려는 것 같습니다. 개체 계층 구조 및 외부 종속성과 독립적으로 단위 테스트를 위해 클래스를 대상으로 지정하는 방법은 dependency injection을 통해 이루어집니다. 과 같이 클래스 "FooFileReader"를 만들기 :

class FooFileReader 
{ 
public: 
    virtual std::ostream& GetFileStream() = 0; 
}; 

이 두 가지 구현, 파일을 열고 스트림으로 노출 한 확인 다른이 (즉 당신이 정말로 필요한 경우 또는 바이트의 배열을.) 알고리즘을 강조하도록 설계된 테스트 데이터를 반환하는 모의 객체. 당신은 모의 객체를 전달하여 단위 테스트에 대한 foo는을 구성하거나, 파일을 엽니 다 구현을 사용하여 실제 파일을 만들 수 있습니다, 지금

Foo(FooFileReader* pReader) 

: 이제

은 푸 생성자이 서명이 있는지 확인 . factory에 "실제"Foo의 구성을 정리하면 클라이언트가 올바른 구현을 쉽게 할 수 있습니다.

이 접근법을 사용하면 "int getChecksum()"에 대한 테스트를하지 않을 이유가 없습니다. 구현이 이제 mock 객체를 사용할 것이기 때문입니다.

+0

+1이 특정 시나리오에서 작동 할 수 있다고 가정합니다. 그러나 레지스트리 키, 파일, 파이프 등 모든 종류의 리소스 관리 클래스에서 작동하는 좀 더 일반적인 대답을 기대합니다. 죄송합니다. 질문에서 꽤 명확하지 않았습니다. ( –

1

간단하고 직접적인 대답은 단위 테스트 클래스를 테스트중인 클래스의 친구로 만드는 것입니다. 이렇게하면 단위 테스트 클래스는 개인용이지만 calculateChecksum()에 액세스 할 수 있습니다.

또 다른 가능성은 Foo가 많은 관련이없는 것으로 보이며 다시 고려해야하기 때문일 수 있습니다. 아마도 체크섬 계산은 실제로 Foo의 일부가되어서는 안됩니다. 대신 체크섬을 계산하는 것은 누구나 필요에 따라 적용 할 수있는 범용 알고리즘 (또는 역도의 종류 - std::accumulate과 같은 다른 알고리즘과 함께 사용하는 펑터)보다 나을 수도 있습니다.

+0

+1 나는이 아이디어가 마음에 든다. boost :: test로 할 수있는 방법이 있는가? –

+0

@BillyONeal : 별도의 질문으로 물어 보는 것이 더 나을 것입니다. - 확신 할 수는 없지만 다른 사람 (이 의견을 볼 확률이 적음)은 잘 할 수 있습니다. –

+0

OK : 할 것입니다. 고맙습니다! –

3

한 가지 방법은 체크섬 방법을 자체 클래스에 추출하고 테스트 할 공용 인터페이스를 만드는 것입니다.나는 그것을 자신의 클래스로 체크섬 계산 코드를 추출하여 시작 했죠

+0

우선 체크섬 방법을'public'으로 만드는 것보다이게 낫겠습니까? –

+2

물론 체크섬을 Foo에서 공개하지 않으려는 것일뿐입니다. 내 생각에 Foo가 체크섬을 계산하는 것은 실제로 Foo의 책임이 아니며 클래스 자체에 클래스를 가져와야한다는 생각이 들었습니다. 그렇게하면 테스트 한 것과 같은 방식으로 테스트하게됩니다. Foo에서 사용하십시오. – NobodyReally

1

는 :

class CheckSumCalculator { 
    std::wstring fileName_; 

public: 
    CheckSumCalculator(const std::wstring& fileName) : fileName_(fileName) 
    { 
    }; 

    int doCalculation() 
    { 
     // Complex logic to calculate a checksum 
    } 
}; 

이 매우 쉽게 분리의 체크섬 계산을 테스트 할 수 있습니다. 당신은 그러나 한 걸음 더 나아가 그것을 가지고 간단한 인터페이스를 만들 수 있습니다

class FileCalculator { 

public: 
    virtual int doCalculation() =0; 
}; 

및 구현 :

class CheckSumCalculator : public FileCalculator { 
    std::wstring fileName_; 

public: 
    CheckSumCalculator(const std::wstring& fileName) : fileName_(fileName) 
    { 
    }; 

    virtual int doCalculation() 
    { 
     // Complex logic to calculate a checksum 
    } 
}; 

을 다음 Foo 생성자에 FileCalculator 인터페이스를 전달합니다

class Foo { 
    std::wstring fileName_; 
    FileCalculator& fileCalc_; 
public: 
    Foo(const std::wstring& fileName, FileCalculator& fileCalc) : 
     fileName_(fileName), 
     fileCalc_(fileCalc) 
    { 
     //Construct a Foo here. 
    }; 

    int getChecksum() 
    { 
     //Open the file and read some part of it 

     return fileCalc_.doCalculation(something); 
    } 

    void modifyThisFileSomehow() 
    { 
     //Perform modification 

     int newChecksum = fileCalc_.doCalculation(something); 

     //Apply the newChecksum to the file 
    } 
}; 

실제 생산 코드에서는 CheckSumCalculator을 만들고이를 Foo으로 전달하지만 단위 테스트 코드 Fake_CheckSumCalculator을 만들 수 있습니다 (예 : 항상 미리 정의 된 체크섬을 반환 함).

이제 Foo에 대한 의존성이 CheckSumCalculator인데도이 두 클래스를 완벽하게 분리하여 테스트 할 수 있습니다.

+0

예,이 방법이 효과가 있다고 생각하지만 처음에는 체크섬 계산을 '비공개로 설정'하는 모든 과정을 중단하지 않습니까? 이것은 매우 원형 인 것 같습니다. 함수를 public으로 만드는 것. 이것이 캡슐화를 공개 기능을 향상시키는 것보다 어떻게 향상시킬 수 있습니까? –

+1

BillyONeal : '비공개'는 데이터를 보호하고 데이터가없는 작업은 엉터리이며 보호 할 필요가 없습니다. –

+0

디자인 트레이드 오프를 선택해야한다면 테스트 하네스에서 독립적으로 테스트 및 테스트 할 수있는 더 작은 클래스를 사용하는 대신 테스트하기 어려운 많은 '전용'메소드가있는보다 큰 클래스를 사용하는 것이 좋습니다. 이것은 당신의 라이브러리의 "내부 작동"이 현재 세계에 노출되어 있음을 의미 할 수도 있지만 결과가 더 잘 테스트 된 코드 일 경우 대개 가치가 있습니다. –

1
#ifdef TEST 
#define private public 
#endif 

// access whatever you'd like to test here 
+0

와우, +1 순수한 악의에! –

+0

테스트 코드와 실제 코드가 함께 컴파일 된 경우이 작업이 가능합니다. 불행히도 테스트중인 코드는 정적 라이브러리에 있고 단위 테스트는 .EXE에 있습니다. : –

+0

개인/공용 액세스는 컴파일러에 의해서만 실행되며 런타임에는 존재하지 않으므로 동일한 방식으로 작동합니다. 사용중인 헤더의 포함 전에이 정의를 사용하면 – rmn

0

파일 IO의 기본 방법은 스트림입니다. 그래서 위의 예제에서 파일 이름 대신 스트림을 삽입하는 것이 훨씬 더 합리적 일 것입니다. 예를 들어, 단위 테스트에 대한 std::stringstream 사용할 수있는 방법으로

Foo(const std::stream& file) : file_(file) 

는 시험을 완벽하게 제어 할 수 있습니다.

스트림을 사용하지 않으려는 경우 File 클래스를 정의하는 RAII 패턴의 표준 예제를 사용할 수 있습니다. 계속 진행하는 "간단한"방법은 순수한 가상 인터페이스 클래스 File을 만든 다음 인터페이스를 구현하는 것입니다. Foo 클래스는 File 클래스 인터페이스를 사용합니다. 예를 들어,

Foo(const File& file) : file_(file) 

테스트는 단순히 File하는 간단한 서브 클래스를 생성하는 대신 (스텁) 것을 주입하여 수행된다. mock 클래스 만들기 (예를 들어 Google Mock 참조)도 수행 할 수 있습니다.

그러나 구현 단위도 File 단위로 테스트하고 RAII이므로 순차적으로 종속성 주입이 필요합니다. 나는 보통 기본적인 C 파일 작업 (열기, 닫기, 읽기, 쓰기 등등 또는 fopen, fclose, fwrite, fread 등)을 제공하는 순수 가상 인터페이스 클래스를 작성하려고합니다. 예를 들어,

class FileHandler { 
public: 
    virtual ~FileHandler() {} 
    virtual int open(const char* filename, int flags) = 0; 
    // ... and all the rest 
}; 

class FileHandlerImpl : public FileHandlerImpl { 
public: 
    virtual int open(const char* filename, int flags) { 
     return ::open(filename, flags); 
    } 
    // ... and all the rest in exactly the same maner 
}; 

FileHandlerImpl 클래스는 내가 단위 테스트 그것을하지 않도록 간단하다. 그러나 이점은 FileImpl 클래스의 생성자에서이 클래스를 사용하면 FileImpl 클래스를 쉽게 단위 테스트 할 수 있다는 것입니다. 예를 들어,

FileImple(const FileHandler& fileHandler, const std::string& fileName) : 
    mFileHandler(fileHandler), mFileName(fileName) 

는 유일한 단점 지금까지 FileHandler 주위에 전달한다는 것이다. FileHandle 인터페이스를 사용하여 실제로 FileHandler 개체의 단일 전역 인스턴스를 가져 오는 데 사용할 수있는 정적 인스턴스 set/get-methods를 제공 할 생각이었습니다.싱글 톤이 아니기 때문에 단위 테스트가 가능하지만 우아한 솔루션은 아닙니다. 처리기를 전달하는 것이 가장 좋은 방법이라고 생각합니다.