재 설계 할 때 어떻게 효율적으로 테스트를 유지합니까?


14

잘 테스트 된 코드베이스는 여러 가지 장점이 있지만 시스템의 특정 측면을 테스트하면 일부 유형의 변경에 저항하는 코드베이스가 생성됩니다.

예를 들어 텍스트 또는 HTML과 같은 특정 출력을 테스트하는 것입니다. 테스트는 종종 특정 텍스트 블록을 일부 입력 매개 변수에 대한 출력으로 예상하거나 블록에서 특정 섹션을 검색하기 위해 작성됩니다 (순진하게?).

새로운 요구 사항을 충족 시키거나 사용성 테스트로 인해 인터페이스가 변경 되었기 때문에 코드의 동작을 변경하려면 테스트도 변경해야합니다. 특히 변경되는 코드에 대한 단위 테스트가 아닌 테스트 일 수도 있습니다.

  • 이러한 테스트를 찾아 다시 작성하는 작업을 어떻게 관리합니까? "모두 실행하고 프레임 워크가 정렬되도록"할 수 없다면 어떨까요?

  • 습관적으로 깨지기 쉬운 테스트를하는 다른 종류의 테스트 대상 코드는 무엇입니까?


이것은 programmers.stackexchange.com/questions/5898/… 과 어떻게 다른가 ?
AShelly

4
이 질문은 실수로 리팩토링에 대해 물었습니다. 단위 테스트는 리팩토링 하에서 변하지 않아야합니다.
Alex Feinman

답변:


9

나는 TDD 사람들 이이 대답을 싫어할 것이라는 것을 알고 있지만, 그것을 위해 많은 부분을 테스트 할 곳을 신중하게 선택해야합니다.

하위 계층의 단위 테스트에 너무 열중하면 단위 테스트를 변경하지 않고 의미있는 변경을 수행 할 수 없습니다. 인터페이스가 노출되지 않고 앱 외부에서 재사용되지 않는 경우에는 빠른 변경이 있었을 수도있는 불필요한 오버 헤드 일뿐입니다.

반대로, 변경하려는 내용이 노출되거나 재사용되는 경우 변경해야 할 테스트 중 하나라도 다른 곳에서 침입 할 수 있다는 증거입니다.

일부 프로젝트에서는 단위 테스트가 아닌 승인 단계에서 테스트를 설계 할 수 있습니다. 더 적은 단위 테스트와 더 많은 통합 스타일 테스트가 있습니다.

그렇다고 해당 기능이 승인 기준을 충족 할 때까지 단일 기능과 코드를 식별 할 수는 없습니다. 그것은 어떤 경우에는 단위 테스트로 합격 기준을 측정하지 않는 것을 의미합니다.


"앱 외부"가 아니라 "모듈 외부"를 작성하려고했다고 생각합니다.
SamB

SamB, 그것은 달려 있습니다. 인터페이스가 하나의 앱이있는 몇 곳의 내부이지만 공용이 아닌 경우 인터페이스가 변동성이 있다고 생각되면 더 높은 수준에서 테스트하는 것이 좋습니다.
Bill

이 방법이 TDD와 매우 호환되는 것으로 나타났습니다. 최종 사용자와 가까운 응용 프로그램의 상위 계층에서 시작하는 것을 좋아하므로 상위 계층에서 하위 계층을 어떻게 사용해야하는지 알고 하위 계층을 디자인 할 수 있습니다. 본질적으로 하향식을 구축하면 한 계층과 다른 계층 사이의 인터페이스를보다 정확하게 설계 할 수 있습니다.
Greg Burghardt

4

방금 전체 SIP 전송을 다시 작성하여 SIP 스택을 대대적으로 점검했습니다. (이것은 대부분의 리팩토링에 비해 거의 대규모의 리 팩터였습니다.)

간단히 말해 TIdSipTransport의 하위 클래스 인 TIdSipTcpTransport가 있습니다. 모든 TIdSipTransports는 공통 테스트 스위트를 공유합니다. TIdSipTcpTransport의 내부에는 연결 / 시작 메시지 쌍, 스레드 TCP 클라이언트, 스레드 TCP 서버 등을 포함하는 맵이 여러 개있었습니다.

내가 한 일은 다음과 같습니다.

  • 교체하려는 클래스를 삭제했습니다.
  • 해당 클래스의 테스트 스위트를 삭제했습니다.
  • 왼쪽 TIdSipTcpTransport에 테스트 스위트 특정을 (그리고 테스트 스위트 일반적인 모든 TIdSipTransports 여전히 있었다).
  • TIdSipTransport / TIdSipTcpTransport 테스트를 실행하여 모두 실패했는지 확인하십시오.
  • TIdSipTransport / TIdSipTcpTransport 테스트를 제외하고 모두 주석 처리했습니다.
  • 클래스를 추가해야 할 경우 주석 처리되지 않은 유일한 테스트가 통과 한 충분한 기능을 빌드하기 위해 쓰기 테스트를 추가합니다.
  • 오히려 헹구고 반복하십시오.

따라서 주석 처리 된 테스트 (*) 형식으로 여전히 필요한 작업을 알고 새로운 테스트 덕분에 새 코드가 예상대로 작동한다는 것을 알았습니다.

(*) 사실, 당신은 그것들을 언급 할 필요가 없습니다. 그냥 실행하지 마십시오. 100 번의 실패한 테스트는 그리 고무적이지 않습니다. 또한 내 특정 설정에서 적은 수의 테스트를 컴파일하면 테스트 쓰기 리 팩터 루프가 빨라집니다.


나는 몇 달 전에 이것을 해왔으며 그것은 나를 위해 아주 잘 작동했습니다. 그러나 도메인 모델 모듈의 획기적인 재 설계에서 동료와 페어링 할 때이 방법을 절대 적용 할 수 없었습니다.
Marco Ciambrone 2016 년

3

테스트가 깨지기 쉬운 경우, 나는 잘못된 것을 테스트하기 때문에 일반적으로 발견합니다. HTML 출력을 예로 들어 보겠습니다. 실제 HTML 출력을 확인하면 테스트가 약해집니다. 그러나 실제 출력에는 관심이없고, 정보를 전달해야하는지에 관심이 있습니다. 불행히도 그렇게하려면 사용자 두뇌의 내용에 대한 주장이 필요하므로 자동으로 수행 할 수 없습니다.

당신은 할 수 있습니다 :

  • HTML을 연기 테스트로 생성하여 실제로 실행되는지 확인하십시오.
  • 템플릿 시스템을 사용하면 정확한 템플릿 자체를 실제로 테스트하지 않고도 템플릿 프로세서 및 템플릿으로 전송 된 데이터를 테스트 할 수 있습니다.

SQL에서도 같은 일이 발생합니다. 실제 SQL을 주장하면 클래스가 문제를 일으키려고 시도합니다. 당신은 정말로 결과를 주장하고 싶습니다. 따라서 단위 테스트 중에 SQLITE 메모리 데이터베이스를 사용하여 SQL이 실제로 예상대로 작동하는지 확인합니다.


또한 구조적 HTML을 사용하는 데 도움이 될 수 있습니다.
SamB

그 도움이 될 확실히 @SamB,하지만 난 그것을 완전히 문제를 해결할 수 있습니다 생각하지 않는다
윈스턴 Ewert

물론, 아무것도 할 수 없습니다 :-)
SamB

-1

먼저 새 API 동작을 원하는 새 API를 작성하십시오. 이 새 API의 이름이 OLDER API와 동일한 경우 새 API 이름에 _NEW라는 이름을 추가합니다.

int DoSomethingInterestingAPI ();

된다 :

int DoSomethingInterestingAPI_NEW (int takes_more_arguments); int DoSomethingInterestingAPI_OLD (); int DoSomethingInterestingAPI () {DoSomethingInterestingAPI_NEW (whatever_default_mimics_the_old_API); 이 단계에서는 DoSomethingInterestingAPI ()라는 이름을 사용하여 모든 회귀 테스트가 통과합니다.

다음으로 코드를 살펴보고 DoSomethingInterestingAPI ()에 대한 모든 호출을 적절한 DoSomethingInterestingAPI_NEW () 변형으로 변경하십시오. 여기에는 새로운 API를 사용하기 위해 회귀 테스트의 일부를 변경 / 갱신하는 것이 포함됩니다.

다음으로 DoSomethingInterestingAPI_OLD ()를 [[deprecated ()]]로 표시하십시오. 더 이상 사용되지 않는 API를 유지하십시오 (원하는 코드를 모두 안전하게 업데이트 할 때까지).

이 방법을 사용하면 회귀 테스트의 실패는 단순히 해당 회귀 테스트의 버그이거나 원하는대로 코드의 버그를 식별합니다. _NEW 및 _OLD 버전의 API를 명시 적으로 작성하여 API를 수정하는 단계적 프로세스를 통해 한동안 새롭고 오래된 코드가 공존 할 수 있습니다.

실제로이 접근법의 좋은 예는 다음과 같습니다. BitSubstring () 함수를 사용했습니다. 여기서 세 번째 매개 변수를 하위 문자열의 비트 수로 만드는 접근법을 사용했습니다. C ++의 다른 API 및 패턴과 일관성을 유지하기 위해 함수의 인수로 시작 / 종료로 전환하고 싶었습니다.

https://github.com/SophistSolutions/Stroika/commit/003dd8707405c43e735ca71116c773b108c217c0

새 API로 BitSubstring_NEW 함수를 만들고 모든 코드를 업데이트하여 사용했습니다 (BitSubString에 더 이상 호출하지 않음). 그러나 여러 릴리스 (개월) 동안 구현을 떠나서 더 이상 사용되지 않음으로 표시하여 모든 사람이 BitSubString_NEW로 전환 할 수 있습니다 (그 당시 인수를 카운트에서 시작 / 종료 스타일로 변경).

그런 다음 전환이 완료되면 BitSubString ()을 삭제하고 BitSubString_NEW-> BitSubString ()의 이름을 바꾸고 또 다른 커밋을 수행했습니다 (BitSubString_NEW라는 이름은 더 이상 사용되지 않습니다).


의미가 없거나 접미사로 붙지 않는 접미사를 추가하지 마십시오. 항상 의미있는 이름을 부여하려고 노력하십시오.
Basilevs

당신은 요점을 완전히 놓쳤다. 첫째, "무의미한"접미사가 아닙니다. API가 이전 버전에서 최신 버전으로 전환되고 있다는 의미를 갖습니다. 사실, 그것은 내가 대답 한 질문의 요점과 답의 요점입니다. CLEARLY는 NEW API 인 OLD API이며 전환이 완료되면 API의 최종 대상 이름입니다. AND-_OLD / _NEW 접미사는 일시적입니다. API 변경 전환 중에 만 가능합니다.
루이스 프링 글

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