단위 테스트 응용 프로그램 논리와 불신 언어 구성 사이의 경계는 어디에 있습니까?


87

다음과 같은 기능을 고려하십시오.

function savePeople(dataStore, people) {
    people.forEach(person => dataStore.savePerson(person));
}

다음과 같이 사용될 수 있습니다.

myDataStore = new Store('some connection string', 'password');
myPeople = ['Joe', 'Maggie', 'John'];
savePeople(myDataStore, myPeople);

자체 단위 테스트가 있거나 공급 업체에서 제공 한다고 가정 해 봅시다 Store. 어쨌든 우리는 신뢰 Store합니다. 그리고 데이터베이스 연결 끊기 오류와 같은 오류 처리는 그 책임이 아니라고 가정합니다 savePeople. 실제로, 상점 자체 는 어떤 식 으로든 오류를 일으킬 수없는 마법의 데이터베이스 라고 가정 해 봅시다 . 이러한 가정이 주어지면 질문은 다음과 같습니다.

savePeople()단위 테스트를 해야합니까 , 아니면 그러한 테스트가 내장 forEach언어 구성체 를 테스트하는 것 입니까?

물론 우리는 모의 dataStore를 건네고 dataStore.savePerson()각 사람마다 한 번씩 부름을받을 수 있습니다. 그러한 테스트가 구현 변경에 대해 보안을 제공한다고 주장 할 수 있습니다. 예를 들어, forEach전통적인 for루프 로 대체하기로 결정한 경우 또는 다른 반복 방법. 그래서 테스트가 아닌 완전히 사소한. 그럼에도 불구하고 끔찍한 것 같습니다 ...


더 유익한 또 다른 예가 있습니다. 다른 객체 나 함수를 조정하는 것 외에는 아무것도하지 않는 함수를 고려하십시오. 예를 들면 다음과 같습니다.

function bakeCookies(dough, pan, oven) {
    panWithRawCookies = pan.add(dough);
    oven.addPan(panWithRawCookies);
    oven.bakeCookies();
    oven.removePan();
}

당신이 생각한다고 가정 할 때, 이와 같은 기능은 어떻게 단위 테스트를해야합니까? 나를 단순히 조롱하지 않습니다 단위 테스트의 모든 종류의 상상하는 것은 어렵다 dough, pan그리고 oven, 다음 방법을 그들이라고 주장합니다. 그러나 이러한 테스트는 함수의 정확한 구현을 복제하는 것 이상을 수행하지 않습니다.

의미있는 블랙 박스 방식으로 기능을 테스트 할 수없는 기능이 기능 자체의 설계 결함을 나타 냅니까? 그렇다면 어떻게 개선 할 수 있습니까?


bakeCookies예제의 동기를 부여하는 질문에 대해보다 명확하게하기 위해 보다 현실적인 시나리오를 추가하겠습니다.이 시나리오는 레거시 코드에 테스트를 추가하고 리팩토링하려고 할 때 발생하는 시나리오입니다.

사용자가 새 계정을 만들 때 다음과 같은 여러 가지 상황이 발생해야합니다. 1) 데이터베이스에 새 사용자 레코드를 작성해야합니다. 2) 환영 이메일을 보내야합니다. 3) 사기를 위해 사용자의 IP 주소를 기록해야합니다. 목적.

따라서 모든 "새 사용자"단계를 함께 묶는 메소드를 작성하려고합니다.

function createNewUser(validatedUserData, emailService, dataStore) {
  userId = dataStore.insertUserRecord(validateduserData);
  emailService.sendWelcomeEmail(validatedUserData);
  dataStore.recordIpAddress(userId, validatedUserData.ip);
}

이러한 메소드 중 하나라도 오류가 발생하면 오류가 호출 코드까지 버블 링되어 오류가 적절하다고 판단 될 때 오류를 처리 할 수 ​​있습니다. API 코드에 의해 호출되면 오류가 적절한 http 응답 코드로 변환 될 수 있습니다. 웹 인터페이스에 의해 호출되면 오류가 사용자에게 표시되는 적절한 메시지로 변환 될 수 있습니다. 요점은 이 함수 는 발생할 수있는 오류를 처리하는 방법을 모른다는 것입니다.

혼동의 본질은 그러한 기능을 단위 테스트하려면 테스트 자체에서 정확한 구현을 반복해야하며 (메소드가 특정 순서로 모의 객체에 호출되도록 지정하여) 잘못된 것 같습니다.


44
실행 후. 쿠키가 있습니까
Ewan

6
업데이트 관련 : 왜 팬을 조롱하고 싶습니까? 아니면 반죽? 이것들은 생성하기가 쉽지 않은 단순한 메모리 내 객체처럼 들리므로 하나의 단위로 테스트하지 않아야 할 이유가 없습니다. "단위 테스트"에서 "단위"는 "단일 클래스"를 의미하지 않습니다. "무엇을 수행하는 데 사용되는 가장 작은 코드 단위"를 의미합니다. 팬은 아마도 반죽 물체를 담는 용기 일 뿐이므로 bakeCookies 방법을 외부에서 직접 테스트하는 대신 분리하여 테스트하는 것이 좋습니다.
sara

11
하루가 끝나면 여기서 작동하는 기본 원칙은 코드가 작동하는지 확인하고 누군가가 무언가를 변경할 때 적절한 "석탄의 카나리아"라는 충분한 테스트를 작성한다는 것입니다. 그게 다야. 마법의 암시, 공식 가정 또는 독단적 주장이 없기 때문에 85 %에서 90 %의 코드 적용 범위 (100 % 아님)가 널리 인정되는 이유 입니다.
Robert Harvey

5
@RobertHarvey는 불행히도 공식적인 platitudes와 TDD 사운드 바이트를, 당신에게 열정적 인 동의를 얻는 동시에 실제 문제를 해결하는 데 도움이되지는 않습니다. 왜냐하면 당신은 손을 더럽
Jonah

4
순환 복잡성을 감소시키기위한 단위 테스트. 날 믿어,이 기능을 시작하기 전에 시간이 없어 질거야
Neil McGuigan

답변:


118

savePeople()단위 테스트를 받아야합니까 ? 예. dataStore.savePerson작동하는지, db 연결이 작동하는지, 심지어 작동하는지 테스트하지 않습니다 foreach. savePeople계약을 통한 약속 을 이행하는 테스트 중입니다 .

이 시나리오를 상상해보십시오. 누군가 코드베이스를 크게 리팩터링하고 실수로 forEach구현 의 일부를 제거하여 항상 첫 번째 항목 만 저장하도록합니다. 단위 테스트를 통해이를 파악하고 싶지 않습니까?


20
@RobertHarvey : 회색 영역이 많으며, IMO를 구분하는 것은 중요하지 않습니다. "올바른 기능을 호출한다"는 것이 아니라 어떻게 "올바른 일을하는지"테스트하는 것이 중요합니다. 중요한 것은 함수에 대한 특정 입력 세트가 주어지면 특정 출력 세트를 얻는다는 것을 테스트하는 것입니다. 그러나 마지막 문장이 혼란 스러울 수 있으므로 제거했습니다.
Bryan Oakley

64
"당신은 savePeople이 계약을 통해 약속 한 약속을 이행하는지 테스트하고 있습니다." 이. 너무 많은.
Lovis

2
이를 다루는 "종료"시스템 테스트가없는 한.
Ian

6
@Ian End-to-End 테스트는 단위 테스트를 대체하지 않으며 무료입니다. 사람들의 목록을 동굴에 저장하는 엔드-투-엔드 테스트가 있다고해서이를 커버하기 위해 단위 테스트를해서는 안된다는 의미는 아닙니다.
Vincent Savard 2016 년

4
@VincentSavard, 그러나 위험이 다른 방식으로 통제된다면 단위 테스트의 비용 / 혜택이 줄어 듭니다.
Ian

36

일반적으로 이런 종류의 질문은 사람들이 "테스트 후"개발을 수행 할 때 발생합니다. 테스트가 구현 전에 나오는 TDD의 관점에서이 문제에 접근하고 연습으로이 질문을 다시 해보십시오.

적어도 일반적으로 외부에있는 TDD 응용 프로그램에서는 구현 한 savePeople후 와 같은 기능을 구현하지 않을 것 savePerson입니다. savePeoplesavePerson기능 중 하나로 시작하는 것이 동일한 장치에서 테스트 테스트 구동되며; 리팩토링 단계에서 몇 번의 테스트 후에 두 개 사이의 분리가 발생합니다. 이 작업 모드는 기능이 어디에 savePeople있어야하는지에 대한 의문을 제기 할 수 있습니다 dataStore.

결국,이 테스트가 올바르게 저장할 수있는 경우에만 확인하지 것이다 Person에서을 Store뿐만 아니라, 많은 사람들. 예를 들어, " savePeople함수를 모두 저장하거나 저장하지 않고 원자 기능 인지 확인해야 합니까?" 이러한 오류는 어떻게 생겼습니까? "등이 있습니다. 이 모든 것은 단지 하나 forEach또는 다른 형태의 반복 사용을 확인하는 것 이상의 의미가 있습니다 .

한 번에 한 사람 이상을 저장 해야하는 요구 사항 savePerson이 이미 제공된 후에 만 만들었 savePerson다면 새로운 기능을 통해 실행 되는 기존 테스트를 업데이트하여 savePeople처음에 단순히 위임하여 한 사람을 구할 수 있는지 확인하십시오. 그런 다음 새로운 테스트를 통해 둘 이상의 사람에 대한 행동을 테스트하고 행동을 원자 적으로 만들어야하는지 여부를 생각합니다.


4
기본적으로 구현이 아닌 인터페이스를 테스트하십시오.
스눕

8
공정하고 통찰력있는 포인트. 그러나 나는 어떻게 든 내 실제 질문이 회피되고 있다고 느낀다 :) 당신의 대답은 "실제 세계에서 잘 설계된 시스템에서, 문제의 단순화 된 버전이 존재하지 않을 것이라고 생각합니다." 다시 말하지만 공정하지만 더 일반적인 문제의 본질을 강조하기 위해이 단순화 된 버전을 구체적으로 만들었습니다. 예제의 인위적인 특성을 뛰어 넘을 수 없다면 비슷한 기능을 수행 할만한 충분한 이유가 있고 반복과 위임 만 한 다른 예제를 상상할 수 있습니다. 아니면 단순히 불가능하다고 생각하십니까?
요나

@Jonah가 업데이트되었습니다. 나는 그것이 당신의 질문에 조금 더 잘 대답하기를 바랍니다. 이것은 모두 의견에 근거한 것이며이 사이트의 목표에 위배 될 수 있지만 확실히 흥미로운 토론입니다. 그건 그렇고, 나는 전문적인 작업의 관점에서 대답하려고 노력했습니다. 여기서 우리는 잘 테스트되고 우리가 떠날 경우 새로운 관리자를위한 문서화 된 시스템. 개인적이거나 중요하지 않은 (돈도 중요하다) 프로젝트의 경우, 나는 매우 다른 의견을 가지고 있습니다.
MichelHenrich

업데이트 해 주셔서 감사합니다. 정확히 어떻게 테스트 savePeople하시겠습니까? OP의 마지막 단락이나 다른 방법으로 설명했듯이?
요나

1
죄송합니다. "모의가없는"부분을 명확하게 밝히지 않았습니다. 나는 savePerson당신이 제안한 대로 함수에 모의를 사용하지 않을 것을 의미 했다. 대신 더 일반적인 것을 통해 테스트 할 것이다 savePeople. 단위 테스트 는 직접 호출 Store하는 savePeople대신 실행되도록 변경 savePerson되므로 모의는 사용되지 않습니다. 그러나 실제 데이터베이스에서 발생하는 다양한 통합 문제에서 코딩 문제를 분리하고 싶기 때문에 데이터베이스는 존재하지 않아야하므로 여기에는 여전히 mock이 있습니다.
MichelHenrich

21

savePeople ()을 단위 테스트해야합니다

그렇습니다. 그러나 구현과 독립적으로 테스트 조건을 작성하십시오. 예를 들어, 사용 예를 단위 테스트로 전환 :

function testSavePeople() {
    myDataStore = new Store('some connection string', 'password');
    myPeople = ['Joe', 'Maggie', 'John'];
    savePeople(myDataStore, myPeople);
    assert(myDataStore.containsPerson('Joe'));
    assert(myDataStore.containsPerson('Maggie'));
    assert(myDataStore.containsPerson('John'));
}

이 테스트는 여러 가지 작업을 수행합니다.

  • 그것은 기능의 계약을 확인 savePeople()
  • 그것은 구현에 신경 쓰지 않습니다. savePeople()
  • 예제 사용법을 문서화합니다. savePeople()

여전히 데이터 저장소를 조롱 / 스텁 / 가짜로 만들 수 있습니다. 이 경우 명시 적 함수 호출을 확인하지 않고 작업 결과를 확인합니다. 이런 식으로 내 테스트는 향후 변경 / 리 팩터를 위해 준비됩니다.

예를 들어, 데이터 저장소 구현은 saveBulkPerson()미래에 방법을 제공 할 수 있습니다. 이제 savePeople()사용 구현을 변경해 도 예상 saveBulkPerson()대로 saveBulkPerson()작동 하는 한 단위 테스트가 중단되지 않습니다 . 그리고 saveBulkPerson()어떻게 든 예상대로 작동하지 않으면 단위 테스트 가이를 잡을 것입니다.

또는 이러한 테스트가 내장 forEach 언어 구문을 테스트하는 데 도움이됩니까?

말했듯이 구현이 아닌 예상 결과와 함수 인터페이스를 테스트 해보십시오 (통합 테스트를 수행하지 않는 한 특정 함수 호출을 잡는 것이 유용 할 수 있습니다). 함수를 구현하는 여러 방법이있는 경우, 모두 유닛 테스트와 함께 작동해야합니다.

질문 업데이트와 관련하여 :

상태 변경을 테스트하십시오! 예를 들어 반죽 중 일부가 사용됩니다. 구현에 따라 사용 된 양이 사용 된 양에 dough맞는지 pan또는 주장했는지 확인하십시오 dough. pan함수 호출 후 포함에 쿠키가 있다고 가정 하십시오. (가) 주장 oven이전과 동일한 상태 / 비어 있습니다.

추가 테스트의 경우 에지 사례를 확인 oven하십시오. 통화하기 전에 비어 있지 않으면 어떻게됩니까 ? 충분하지 않으면 어떻게됩니까 dough? 이 경우 pan이미 가득?

반죽, 팬 및 오븐 물체 자체에서 이러한 테스트에 필요한 모든 데이터를 추론 할 수 있어야합니다. 함수 호출을 캡처 할 필요가 없습니다. 함수를 구현할 수없는 것처럼 처리하십시오!

실제로 대부분의 TDD 사용자는 함수를 작성하기 전에 테스트를 작성하므로 실제 구현에 의존하지 않습니다.


최근 추가 한 내용 :

사용자가 새 계정을 만들 때 다음과 같은 여러 가지 상황이 발생해야합니다. 1) 데이터베이스에 새 사용자 레코드를 작성해야합니다. 2) 환영 이메일을 보내야합니다. 3) 사기를 위해 사용자의 IP 주소를 기록해야합니다. 목적.

따라서 모든 "새 사용자"단계를 함께 묶는 메소드를 작성하려고합니다.

function createNewUser(validatedUserData, emailService, dataStore) {
    userId = dataStore.insertUserRecord(validateduserData);
    emailService.sendWelcomeEmail(validatedUserData);
    dataStore.recordIpAddress(userId, validatedUserData.ip);
}

이와 같은 기능을 위해 모의 / 스텁 / 가짜 (더 일반적인 것으로 보이는 것) dataStoreemailService매개 변수를 사용합니다. 이 함수는 자체적으로 매개 변수에 대한 상태 전이를 수행하지 않고 일부 매개 변수의 메소드에 위임합니다. 함수 호출이 4 가지 일을했는지 ​​확인하려고합니다.

  • 데이터 저장소에 사용자를 삽입했습니다.
  • 환영 이메일을 보냈습니다.
  • 데이터 저장소에 사용자 IP를 기록했습니다.
  • 발생한 예외 / 오류를 위임했습니다 (있는 경우).

처음 3 검사는 모의 객체, 스텁 또는 가짜와 함께 할 수 dataStoreemailService(당신이 정말로 테스트 할 때 이메일을 보내려고하지 않습니다). 주석 중 일부를 찾아야했기 때문에 다음과 같은 차이점이 있습니다.

  • 가짜는 원본과 동일하게 작동하며 어느 정도 구별 할 수없는 물체입니다. 코드는 일반적으로 여러 테스트에서 재사용 할 수 있습니다. 예를 들어, 데이터베이스 랩퍼에 대한 단순한 인 메모리 데이터베이스가 될 수 있습니다.
  • 스텁은이 테스트의 필수 작업을 수행하는 데 필요한만큼만 구현합니다. 대부분의 경우 스텁은 원본의 작은 방법 세트 만 필요한 테스트 또는 테스트 그룹에 특정합니다. 이 예에서는 and 및 dataStore의 적절한 버전을 구현하는 것일 수 있습니다 .insertUserRecord()recordIpAddress()
  • 모의는 사용 방법을 확인할 수있는 객체입니다 (대부분 메소드 호출을 평가할 수 있도록하여). 나는 그것들을 사용함으로써 실제로 인터페이스에 대한 준수가 아니라 함수 구현을 테스트하려고 시도하지만 여전히 그들의 용도를 가지고 있기 때문에 단위 테스트에서 조금만 사용하려고합니다. 필요한 모의 객체를 만드는 데 도움이되는 많은 모의 프레임 워크가 있습니다.

이러한 메소드 중 하나라도 오류가 발생하면 오류가 호출 코드까지 버블 링되어 오류가 적절하다고 판단 될 때 오류를 처리 할 수 ​​있습니다. API 코드에 의해 호출되면 오류를 적절한 HTTP 응답 코드로 변환 할 수 있습니다. 웹 인터페이스에 의해 호출되면 오류가 사용자에게 표시되는 적절한 메시지로 변환 될 수 있습니다. 요점은이 함수는 발생할 수있는 오류를 처리하는 방법을 모른다는 것입니다.

예상되는 예외 / 오류는 유효한 테스트 사례입니다. 이러한 이벤트가 발생하면 함수가 예상 한대로 동작 함을 확인합니다. 이는 원하는 경우 해당 모의 / 가짜 / 스텁 객체를 던지도록하여 달성 할 수 있습니다.

혼동의 본질은 그러한 기능을 단위 테스트하려면 테스트 자체에서 정확한 구현을 반복해야하며 (메소드가 특정 순서로 모의 객체에 호출되도록 지정하여) 잘못된 것 같습니다.

때때로 이것은 수행되어야합니다 (통합 테스트에서 대부분이 문제에 관심이 있지만). 예상되는 부작용 / 상태 변경을 확인하는 다른 방법이 더 자주 있습니다.

정확한 함수 호출을 확인하면 부서지기 쉬운 단위 테스트가 수행됩니다. 원래 함수를 약간만 변경하면 실패합니다. 이것은 바람직하든 아니든 기능을 변경할 때마다 리팩토링, 최적화, 버그 수정 등의 해당 단위 테스트를 변경해야합니다.

안타깝게도이 경우 단위 테스트는 신뢰성을 일부 잃습니다. 변경되었으므로 변경 후 이전과 동일한 방식으로 작동 한 후에는 기능을 확인하지 않습니다.

예를 들어, oven.preheat()쿠키 베이킹 예제에서 (최적화!)에 전화를 추가하는 사람을 생각해보십시오 .

  • 오븐 객체를 조롱하면 메소드의 관찰 가능한 동작이 변경되지 않았음에도 불구하고 호출 및 테스트 실패를 예상하지 않습니다 (아마도 쿠키 팬이 여전히 있습니다).
  • 테스트 할 메소드 만 추가했는지 아니면 일부 더미 메소드로 전체 인터페이스를 추가했는지에 따라 스텁이 실패하거나 실패하지 않을 수 있습니다.
  • 가짜는 메소드를 구현해야하기 때문에 실패해서는 안됩니다 (인터페이스에 따라)

단위 테스트에서 가능한 한 일반적으로 노력합니다. 구현이 변경되었지만 호출자의 관점에서 보이는 동작이 여전히 동일하면 테스트가 통과해야합니다. 이상적으로, 기존 단위 테스트를 변경해야하는 유일한 경우는 테스트중인 기능이 아니라 버그 수정이어야합니다.


1
문제는 작성하자마자 myDataStore.containsPerson('Joe')기능 테스트 데이터베이스가 있다고 가정한다는 것입니다. 그렇게하면 단위 테스트가 아닌 통합 테스트를 작성하게됩니다.
Jonah

나는 테스트 데이터 저장소에 의존 할 수 있다고 가정하고 (실제인지 모의인지 상관하지 않는다) 모든 것이 설정대로 작동한다고 가정한다. 테스트에서 테스트하려는 유일한 것은 savePeople()해당 데이터 저장소가 예상 된 인터페이스를 구현하는 한 사용자가 제공하는 모든 데이터 저장소에 해당 사용자를 실제로 추가하는 것입니다. 통합 테스트는 예를 들어 내 데이터베이스 랩퍼가 실제로 메소드 호출에 대한 올바른 데이터베이스 호출을 수행하는지 확인하는 것입니다.
hoffmale 2016 년

분명히하기 위해 모의를 사용하는 경우 모의 메서드가 특정 매개 변수를 사용하여 호출 되었다고 주장하면 됩니다. 나중에 모의 상태를 주장 할 수 없습니다. 따라서 테스트중인 메소드를 호출 한 후 데이터베이스 상태에 대해 어설 션을 작성하려면 다음과 같이 myDataStore.containsPerson('Joe')기능적인 db를 사용해야합니다. 이 단계를 수행하면 더 이상 단위 테스트가 아닙니다.
요나

1
실제 데이터베이스 일 필요는 없습니다. 실제 데이터 저장소와 동일한 인터페이스를 구현하는 객체 일뿐입니다 (읽기 : 데이터 저장소 인터페이스에 대한 관련 단위 테스트를 통과 함). 나는 여전히 이것을 모의로 간주합니다. 모의가 배열에 그렇게하기 위해 어떤 방법 으로든 추가 된 모든 것을 저장하고 테스트 데이터 (의 요소 myPeople)가 배열에 있는지 확인하십시오 . IMHO mock은 여전히 ​​실제 객체에서 예상되는 것과 동일한 관찰 가능한 동작을 가져야합니다. 그렇지 않으면 실제 인터페이스가 아닌 mock 인터페이스의 적합성을 테스트하는 것입니다.
hoffmale 2016 년

"모형은 배열에 그렇게하기 위해 어떤 방법 으로든 추가 된 모든 것을 저장하고 테스트 데이터 (myPeople의 요소)가 배열에 있는지 확인하십시오."- "실제"데이터베이스, 임시, 내장 메모리 중 하나. "IMHO 모의는 여전히 실제 물체에서 예상되는 것과 동일한 관찰 가능한 행동을 가져야합니다."-나는 당신이 그것을 옹호 할 수 있다고 생각하지만, 그것은 테스트 문헌이나 유명한 라이브러리에서 "모의"가 의미하는 것이 아닙니다. 봤어요. 모의는 단순히 예상 된 메소드가 예상 된 매개 변수와 함께 호출되는지 검증합니다.
Jonah

13

이러한 테스트가 제공하는 주요 가치는 구현이 리팩터링 가능하다는 것입니다.

나는 내 경력에서 많은 성능 최적화를 수행했으며 종종 N 패턴을 데이터베이스에 저장하고 N 삽입을 수행하는 정확한 패턴과 관련된 문제를 발견했습니다. 일반적으로 단일 명령문을 사용하여 대량 삽입을 수행하는 것이 더 효율적입니다.

반면에, 우리는 또한 조기 최적화를 원하지 않습니다. 일반적으로 한 번에 1-3 명만 저축하면 최적화 된 배치를 작성하는 것이 과도 할 수 있습니다.

적절한 단위 테스트를 통해 위에서 구현 한 방식으로 작성할 수 있으며 최적화해야 할 경우 자동화 된 테스트의 안전망을 사용하여 오류를 발견 할 수 있습니다. 당연히 이것은 테스트 품질에 따라 달라 지므로 자유롭게 테스트하고 잘 테스트하십시오.

이 동작을 단위로 테스트 할 때의 두 번째 장점은 그 목적이 무엇인지를 문서화하는 것입니다. 이 사소한 예는 명백 할 수 있지만 아래의 다음 사항을 고려할 때 매우 중요 할 수 있습니다.

다른 사람들이 지적한 세 번째 장점은 통합 또는 승인 테스트로 테스트하기 매우 어려운 세부 정보를 테스트 할 수 있다는 것입니다. 예를 들어, 모든 사용자를 원자 적으로 저장해야한다는 요구 사항이있는 경우 테스트 케이스를 작성할 수 있습니다. 테스트 사례를 통해 예상대로 작동하는지 알 수 있고 명확하지 않은 요구 사항에 대한 문서 역할을 수행 할 수 있습니다. 새로운 개발자에게.

나는 TDD 강사로부터 얻은 생각을 추가 할 것입니다. 방법을 테스트하지 마십시오. 동작을 테스트하십시오. 즉, savePeople작동하는지 테스트하지 않고 여러 사용자를 한 번의 호출로 저장할 수 있는지 테스트하고 있습니다.

프로그램이 작동하는지 확인하면서 단위 테스트에 대한 생각을 멈추었을 때 품질 단위 테스트 및 TDD 개선 기능을 찾았지만 오히려 코드 단위가 예상 한대로 작동하는지 확인했습니다 . 그것들이 다릅니다. 그들은 그것이 작동하는지 확인하지는 않지만 내가 생각하는대로 작동하는지 확인합니다. 그런 식으로 생각하기 시작하면 관점이 바뀌 었습니다.


대량 삽입 리팩토링 예제가 좋은 예입니다. 그러나 OP에서 제안한 가능한 단위 테스트 ( savePerson목록의 각 사람에 대해 데이터 저장소 모의가 호출 했음)는 대량 삽입 리팩토링으로 중단됩니다. 나에게 그것은 단위 테스트가 좋지 않다는 것을 나타냅니다. 그러나 실제 테스트 데이터베이스를 사용하지 않고 사람에 대해 대량 저장 및 한 사람당 저장을 모두 구현하는 대안은 보이지 않습니다. 두 가지 구현 모두에서 작동하는 테스트를 제공 할 수 있습니까?
요나

1
@ jpmc26 사람들이 구해 졌음을 테스트하는 테스트는 어떻습니까?
immibis 2016 년

1
@immibis 나는 그것이 무엇을 의미하는지 이해하지 못한다. 아마도 실제 저장소는 데이터베이스에 의해 뒷받침되므로 단위 테스트를 위해 그것을 조롱하거나 스터브해야합니다. 따라서 해당 시점에서 모의 ​​객체 또는 스텁이 객체를 저장할 수 있는지 테스트합니다. 그것은 전혀 쓸모가 없습니다. 가장 좋은 savePerson방법은 각 입력에 대해 메소드가 호출 되었다고 주장하는 것입니다 . 루프를 대량 삽입으로 대체하면 더 이상 해당 메소드를 호출하지 않습니다. 따라서 테스트가 중단됩니다. 당신이 다른 것을 염두에두고 있다면, 나는 그것에 열려 있지만 아직 보지 못했습니다. (그리고 내 요점은 보지 못했습니다.)
jpmc26 2016 년

1
@immibis 나는 그것이 유용한 테스트라고 생각하지 않습니다. 가짜 데이터 저장소를 사용한다고해서 실제로 작동 할 것이라는 확신은 없습니다. 내 가짜가 실제처럼 작동하는지 어떻게 알 수 있습니까? 차라리 통합 테스트 모음에서 다루도록하겠습니다. ( 여기서 첫 번째 의견에서 "모든 단위 테스트"를 의미했음을 분명히해야 합니다.)
jpmc26 2016 년

1
@immibis 나는 실제로 내 입장을 재고하고 있습니다. 나는 이와 같은 문제 때문에 단위 테스트의 가치에 대해 회의적이지만, 입력을 가짜로해도 값을 과소 평가하고 있습니다. 내가 않는 입 / 출력 테스트를하는 경향이 있음을 알고 많은 모의 무거운 검사보다 유용하지만, 가짜가 여기에 실제로 문제의 일부와 어쩌면 거부는 입력을 대체 할 수 있습니다.
jpmc26

6

bakeCookies()테스트 해야합니까 ? 예.

당신이 생각한다고 가정 할 때, 이와 같은 기능은 어떻게 단위 테스트를해야합니까? 단순히 반죽, 팬 및 오븐을 조롱하지 않는 방법으로 단위 테스트를 상상 한 다음 그 방법이 필요하다고 주장하기는 어렵습니다.

실제로는 아닙니다. 함수가 무엇을해야하는지 자세히 살펴보십시오. oven개체를 특정 상태 로 설정해야 합니다. 코드를 살펴보면 pandough객체 의 상태가 실제로 중요하지 않은 것으로 보입니다 . 따라서 oven객체를 전달 하거나 조롱하여 함수 호출이 끝날 때 특정 상태에 있는지 확인해야합니다.

다시 말해, 쿠키 bakeCookies()구운 것을 주장해야 합니다 .

매우 짧은 기능의 경우 단위 테스트가 팽팽한 것 이상으로 보일 수 있습니다. 그러나 잊지 마십시오. 프로그램 작성 시간보다 훨씬 오래 지속됩니다. 이 기능은 나중에 변경 될 수도 있고 변경되지 않을 수도 있습니다.

단위 테스트는 두 가지 기능을 수행합니다.

  1. 모든 것이 작동하는지 테스트합니다. 이것은 가장 유용한 기능 단위 테스트이며 질문을 할 때만이 기능을 고려하는 것 같습니다.

  2. 향후 프로그램 수정으로 인해 이전에 구현 된 기능이 중단되지 않는지 확인합니다. 이것은 단위 테스트에서 가장 유용한 기능이며 큰 프로그램에 버그가 들어가는 것을 방지합니다. 프로그램에 기능을 추가 할 때 일반 코딩에는 유용하지만 프로그램을 구현하는 핵심 알고리즘이 프로그램의 관찰 가능한 동작을 변경하지 않고 크게 변경되는 리팩토링 및 최적화에 더 유용합니다.

함수 내부의 코드를 테스트하지 마십시오. 대신 함수가 말하는 기능을 수행하는지 테스트하십시오. 이런 방식으로 단위 테스트 (코드가 아닌 함수 테스트)를 보면 언어 구조 나 응용 프로그램 논리를 테스트하지 않는다는 것을 알게 될 것입니다. API를 테스트하고 있습니다.


답장을 보내 주셔서 감사합니다. 내 두 번째 업데이트를보고 해당 예제에서 함수를 단위 테스트하는 방법에 대한 생각을 하시겠습니까?
요나

실제 오븐이나 가짜 오븐을 사용할 수있을 때 효과적 일 수 있지만 모의 오븐에서는 그다지 효과적이지 않습니다. Mos (Meszaros 정의에 의한)는 호출 된 메소드의 기록 외에는 어떠한 상태도 갖지 않습니다. 필자의 경험은 이와 같은 기능 bakeCookies을 이러한 방식으로 테스트 할 때 리팩터링 중에 응용 프로그램의 관찰 가능한 동작에 영향을 미치지 않는 기능 이 중단되는 경향이 있다는 것입니다.
James_pic

@James_pic입니다. 그리고 네, 이것이 제가 사용하는 모의 정의입니다. 당신의 의견이 있다면, 당신은 이런 경우에 무엇을합니까? 테스트를 잊었습니까? 어쨌든 취해 구현 반복 테스트를 작성 하시겠습니까? 다른 것?
Jonah

@Jonah 나는 일반적으로 통합 테스트를 사용하여 해당 구성 요소를 테스트하거나 (현대 툴링의 품질로 인해 디버깅하기가 더 어렵다는 경고를 발견했습니다) 또는 반 설득력있는 가짜.
James_pic

3

savePeople ()을 단위 테스트해야합니까, 아니면 그러한 테스트가 내장 forEach 언어 구문을 테스트하는 것입니까?

예. 하지만 당신은 할 수 단지 구조를 다시 테스트 할 수있는 방법으로 그것을한다.

여기서주의해야 할 것은 savePerson반쯤 실패 했을 때이 함수가 어떻게 동작 하는가입니다. 어떻게 작동합니까?

이는 단위 테스트로 시행해야하는 미묘한 동작입니다.


예, 미묘한 오류 조건 테스트 해야 한다는 데 동의 하지만 흥미로운 질문은 아닙니다. 정답은 분명합니다. 따라서 내가 구체적으로 언급 한 이유는 내 질문의 목적 상 savePeople오류 처리에 책임을지지 않아야합니다. 다시 명확히하기 위해 가정savePeople책임 만을 목록을 반복하고 다른 방법으로 각 항목의 저장을 위임, 아직 테스트해야?
요나

@Jonah : 단위 테스트를 foreach구성, 그 밖의 조건, 부작용 또는 행동이 아닌 구성 에만 국한 시키겠다고 주장한다면 옳습니다. 새로운 단위 테스트는 그다지 흥미롭지 않습니다.
Robert Harvey

1
@ jonah-반복하고 가능한 한 많이 저장해야합니까? 아니면 오류가 발생합니까? 단일 저장 사용 방법을 알 수 없으므로 결정할 수 없습니다.
Telastyn

1
@ jonah-사이트에 오신 것을 환영합니다. Q & A 형식의 주요 구성 요소 중 하나는 우리가 당신 을 도울 없다는 입니다. 귀하의 질문은 물론 도움이되지만, 사이트를 방문한 다른 많은 사람들이 자신의 질문에 대한 답변을 찾는 데 도움이됩니다. 당신이 한 질문에 대답했습니다. 답변이 마음에 들지 않거나 골대를 옮기는 것이 내 잘못이 아닙니다. 그리고 솔직히 말해서, 다른 대답은 더 기본적으로 똑같은 것을 말하는 것처럼 보입니다.
Telastyn

1
@Telastyn, 단위 테스트에 대한 통찰력을 얻으려고합니다. 내 초기 질문은 명확하지 않았으므로 실제 질문에 대한 대화를 이끌어 내기 위해 설명을 추가하고 있습니다. 당신은 어떻게 든 "옳은"게임에서 당신을 속이는 것을 해석하기로 선택했습니다. 코드 검토 및 SO에 대한 질문에 대답하는 데 수백 시간을 보냈습니다. 나의 목적은 항상 내가 응답하는 사람들을 돕는 것입니다. 그렇지 않다면 그것이 당신의 선택입니다. 내 질문에 대답 할 필요는 없습니다.
요나

3

여기서 중요한 것은 특정 기능에 대한 사소한 관점입니다. 프로그래밍의 대부분은 사소합니다. 값을 할당하고, 수학을하고, 결정을 내립니다.이 경우라면 반복 할 때까지 루프를 계속하십시오. 프로그래밍 언어를 가르치는 모든 책의 첫 5 개 장을 살펴 보았습니다.

테스트 작성이 매우 쉽다는 사실은 디자인이 그렇게 나쁘지 않다는 신호입니다. 테스트하기 쉽지 않은 디자인을 원하십니까?

"그건 절대 변하지 않을 것입니다." 가장 실패한 프로젝트가 시작되는 방식입니다. 단위 테스트는 특정 상황에서 장치가 예상대로 작동하는지 확인합니다. 그것을 전달하면 구현 세부 정보를 잊어 버릴 수 있습니다. 다음 작업을 위해 뇌 공간을 사용하십시오.

예상대로 작동하는 것을 아는 것은 매우 중요하며 대규모 프로젝트, 특히 대규모 팀에서는 쉽지 않습니다. 프로그래머가 공통적으로 가지고있는 것이 있다면, 우리 모두가 다른 사람의 끔찍한 코드를 다루어야한다는 사실입니다. 우리가 할 수있는 최소한의 테스트는 있습니다. 확실치 않은 경우 테스트를 작성하고 계속 진행하십시오.


의견을 보내 주셔서 감사합니다. 좋은 지적입니다. 내가 정말로 대답하고 싶은 질문 (방금 명확히하기 위해 또 다른 업데이트를 추가했습니다)은 위임을 통해 다른 서비스 시퀀스를 호출하는 것 이상의 기능을 테스트하는 적절한 방법입니다. 이러한 경우, "계약서 문서화"에 적합한 단위 테스트는 함수 구현을 다시 언급 한 것으로 보이며, 다양한 모의에서 메소드가 호출된다고 주장합니다. 그러나 이러한 경우 구현과 동일한 테스트는 잘못된 느낌입니다 ....
Jonah

1

savePeople ()을 단위 테스트해야합니까, 아니면 그러한 테스트가 내장 forEach 언어 구문을 테스트하는 것입니까?

이것은 이미 @BryanOakley에 의해 답변되었지만 추가 인수가 있습니다 (추측).

먼저 단위 테스트는 API 구현이 아닌 계약 이행을 테스트하기위한 것입니다. 테스트는 전제 조건을 설정 한 다음 호출하고 효과, 부작용, 불변 및 사후 조건을 확인해야합니다. 테스트 대상을 결정할 때 API 구현은 중요하지 않으며 중요하지 않습니다 .

둘째, 함수가 변경되면 불변량을 확인하기 위해 테스트가 수행됩니다 . 변경되지 않는 사실 지금은 당신이 테스트를하지 말았어야 의미하지 않는다.

셋째, TDD 접근법 (필수)과 외부에서 사소한 테스트를 구현 한 것이 가치가있다.

C ++을 작성할 때 클래스에 대해 객체를 인스턴스화하고 불변량 (할당 가능, 일반 등)을 확인하는 간단한 테스트를 작성하는 경향이 있습니다. 개발 중에이 테스트가 몇 번이나 깨지는 지 놀랍습니다 (예 : 실수로 수업에 움직일 수없는 멤버 추가).


1

귀하의 질문은 다음과 같이 요약됩니다.

통합 테스트가되지 않고 void 함수를 단위 테스트하는 방법은 무엇입니까?

예를 들어 쿠키 베이크 기능을 변경하여 쿠키를 반환하면 테스트가 무엇인지 즉시 알 수 있습니다.

함수를 호출 한 후 pan.GetCookies를 호출 해야하는 경우 '실제로 통합 테스트'인지 '팬 개체를 테스트하지 않습니까?'

모든 것을 조롱하고 xy 및 z 기능을 검사하는 것이 단위 가치가 없다는 것이 정확하다고 생각합니다.

그러나! 이 경우 테스트 가능한 결과를 반환하거나 실제 객체를 사용하고 통합 테스트를 수행하기 위해 void 함수를 리팩터링해야한다고 주장합니다.

--- createNewUser 예제 업데이트

  • 데이터베이스에 새로운 사용자 레코드를 작성해야합니다
  • 환영 이메일을 보내야합니다
  • 사기 목적으로 사용자의 IP 주소를 기록해야합니다.

이번에는 함수의 결과가 쉽게 반환되지 않습니다. 매개 변수의 상태를 변경하고 싶습니다.

이것은 내가 약간 논쟁의 여지가있는 곳입니다. 상태 저장 매개 변수에 대한 구체적인 모의 구현을 만듭니다.

독자 여러분, 분노를 불러 일으키십시오!

그래서...

var validatedUserData = new UserData(); //we can use the real object for this
var emailService = new MockEmailService(); //a simple mock which saves sentEmails to a List<string>
var dataStore = new MockDataStore(); //a simple mock which saves ips to a List<string>

//run the test
target.createNewUser(validatedUserData, emailService, dataStore);

//check the results
Assert.AreEqual(1, emailService.EmailsSent.Count());
Assert.AreEqual(1, dataStore.IpsRecorded.Count());
Assert.AreEqual(1, dataStore.UsersSaved.Count());

이것은 테스트중인 메소드의 구현 세부 사항을 원하는 동작과 분리합니다. 대체 구현 :

function createNewUser(validatedUserData, emailService, dataStore) {
  userId = dataStore.bulkInsedrtUserRecords(new [] {validateduserData});
  emailService.addEmailToQueue(validatedUserData);
  emailService.ProcessQueue();
  dataStore.recordIpAddress(userId, validatedUserData.ip);
}

여전히 단위 테스트를 통과합니다. 또한 테스트 전체에서 모의 ​​객체를 재사용하고 UI 또는 통합 테스트를 위해 응용 프로그램에 주입 할 수 있다는 이점이 있습니다.


통합 테스트는 단순히 두 가지 구체적인 클래스의 이름을 언급했기 때문에 통합 테스트가 아닙니다 ... 통합 테스트는 디스크 IO, DB, 외부 웹 서비스 등과 같은 외부 시스템과의 통합 테스트에 관한 것입니다. -메모리, 빠르고, 우리가 관심있는 것을 점검합니다. 나는 쿠키를 직접 반환하는 방법이 더 나은 디자인처럼 느껴진다는 것에 동의합니다.
새라

3
기다림. pan.getcookies는 요리사에게 기회가 생겼을 때 쿠키를 오븐에서 꺼낼 것을 요청하는 이메일을 보냅니다.
Ewan

이론적으로는 가능하지만 이름이 잘못 될 수 있습니다. 누가 이메일을 보낸 오븐 장비에 대해 들어 본 적이 있습니까? 그러나 나는 당신이 지적하는 것을 보았습니다. 이 공동 작업 클래스는 리프 객체이거나 단순한 메모리 내 사물이라고 가정하지만 비열한 일을하는 경우주의가 필요합니다. 전자 메일 전송은 이보다 더 높은 수준에서 수행되어야한다고 생각합니다. 이것은 엔터티의 다운 및 더티 비즈니스 로직처럼 보입니다.
사라

2
수사 학적 질문이지만 "전자 메일을 보낸 오븐 장비에 대해 들어 본 사람이 있습니까?" venturebeat.com/2016/03/08/…
clacke

안녕 이완, 나는이 답변이 내가 정말로 요구하는 것에 가장 근접해 있다고 생각합니다. bakeCookies구운 쿠키 반품에 대한 귀하의 요점은 적절하다고 생각하며 게시 후 약간의 생각이있었습니다. 그래서 나는 이것이 다시는 좋은 예가 아니라고 생각합니다. 내 질문에 동기를 부여하는 것에 대한 현실적인 예를 제공하는 업데이트를 하나 더 추가했습니다. 귀하의 의견에 감사드립니다.
요나

0

당신은 또한 테스트해야합니다 bakeCookies-예를 들어 무엇을 bakeCookies(egg, pan, oven)생산 해야 합니까? 계란 후라이 또는 예외? 그들 스스로 는 실제 재료에 대해 신경 쓰거나 신경 쓰지 pan않을 ovenbakeCookies입니다. 보다 일반적으로, dough획득 방법 및 그것이 단순히 또는 예를 들어 대신 될 가능성 있는지 에 따라 달라질 수있다 .eggwater

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