리팩토링 할 때 단위 테스트가 어떻게 작동합니까?


29

또 다른 질문에서, TDD의 어려움 중 하나는 리팩토링 중 및 리팩토링 후 테스트 스위트를 코드베이스와 동기화하는 것으로 밝혀졌습니다.

저는 리팩토링의 열렬한 팬입니다. 나는 TDD를 포기하지 않을 것입니다. 그러나 사소한 리팩토링으로 인해 많은 테스트 실패가 발생하는 방식으로 작성된 테스트 문제도 경험했습니다.

리팩토링시 테스트 중단을 어떻게 피합니까?

  • 더 나은 테스트를 작성합니까? 그렇다면 무엇을 찾아야합니까?
  • 특정 유형의 리팩토링을 피합니까?
  • 테스트 리팩토링 도구가 있습니까?

편집 : 나는 내가 무엇을 요구하는지 묻는 새로운 질문 썼습니다 (그러나이 질문을 흥미로운 변형으로 유지했습니다).


7
TDD를 사용하면 리팩토링의 첫 단계는 실패한 테스트를 작성한 다음 코드를 리팩터링하여 작동시키는 것입니다.
Matt Ellen

IDE가 테스트를 리팩터링하는 방법을 알 수 없습니까?

(당신은 무슨 말을 본질적으로 azheglov의 대답은 볼 수 있지만 흥미로운 변종이 하나를 유지) Thorbjørn Ravn 안데르센 @, 그래, 그리고 내가 물어 무엇을 의미하는지 물었다 새 질문 작성
알렉스 Feinman

이 질문에 thar Info를 추가 하시겠습니까?

답변:


35

당신이하려는 것은 실제로 리팩토링이 아닙니다. 리팩토링으로, 정의에 의해, 당신은 변경되지 않습니다 어떤 소프트웨어가하는, 당신은 변경 어떻게 그것을 않습니다.

모든 테스트 녹색 (전체 패스) 후 (베이스 클래스로부터 파생하는 방법 등을 이동하는 방법을 추출하거나 캡슐화 "후드"수정을 시작 합성을 로모그래퍼 빌더 등). 테스트는 여전히 통과해야합니다.

당신이 묘사하는 것은 리팩토링이 아니라 재 설계이며 테스트중인 소프트웨어의 기능을 향상시킵니다. TDD와 리팩토링 (여기서 정의하려고 시도한)은 충돌하지 않습니다. 여전히 "델타"기능을 개발하기 위해 리팩터링 (녹색-녹색)하고 TDD (적색-녹색)를 적용 할 수 있습니다.


7
동일한 코드 X가 15 자리를 복사했습니다. 각 장소에서 맞춤형. 이를 공통 라이브러리로 만들고 X를 매개 변수화하거나 전략 패턴을 사용하여 이러한 차이를 허용하십시오. X에 대한 단위 테스트가 실패 할 것이라고 보장합니다. 공용 인터페이스가 약간 변경되어 X의 클라이언트가 실패합니다. 재 설계 또는 리팩터링? 나는 그것을 리 팩터라고 부르지 만 어떤 식 으로든 모든 종류의 것들을 깨뜨립니다. 결론은 모든 것이 어떻게 맞는지 정확히 알지 못하면 리팩터링 할 수 없다는 것입니다. 그런 다음 테스트 수정은 지루하지만 궁극적으로는 사소합니다.
Kevin

3
테스트에 지속적인 조정이 필요한 경우에는 너무 자세한 테스트가 필요합니다. 예를 들어 특정 상황에서 특정 순서로 이벤트 A, B 및 C를 트리거해야하는 코드가 있다고 가정합니다. 이전 코드는 ABC 순서대로 수행하며 테스트는 해당 순서대로 이벤트를 예상합니다. 리팩토링 된 코드가 ACB 순서로 이벤트를 뱉어 내더라도 스펙에 따라 작동하지만 테스트는 실패합니다.
otto

3
@Kevin : 공개 인터페이스가 바뀌기 때문에 당신이 묘사 한 것이 재 설계라고 생각합니다. Fowler의 리팩토링 정의 ( "외부 동작을 변경하지 않고 코드의 내부 구조를 변경")는 분명합니다.
azheglov

3
@azheglov : 어쩌면 내 경험상 구현이 나쁘면 인터페이스도 마찬가지입니다.
Kevin

2
완벽하게 유효하고 명확한 질문은“단어의 의미”토론으로 이어집니다. 당신이 그것을 어떻게 부르는지 누가 신경 쓰는지, 다른 곳에서 그 토론을합시다. 그 사이에이 답변은 실제 답변을 완전히 생략했지만 여전히 가장 많은 의견을 제시했습니다. 사람들이 TDD를 종교라고 부르는 이유를 알 수 있습니다.
Dirk Boer

21

단위 테스트를 통해 얻을 수있는 이점 중 하나는 자신있게 리팩토링 할 수 있다는 것입니다.

리팩토링이 공용 인터페이스를 변경하지 않으면 단위 테스트를 그대로두고 리팩토링 후 모두 통과하는지 확인하십시오.

리팩토링이 공용 인터페이스를 변경하는 경우 테스트를 먼저 다시 작성해야합니다. 새로운 테스트가 통과 될 때까지 리팩터링하십시오.

리팩토링은 테스트를 중단하기 때문에 결코 피하지 않을 것입니다. 단위 테스트 작성은 엉덩이에 통증이 될 수 있지만 장기적으로 고통의 가치가 있습니다.


7

다른 답변과 달리 테스트가 화이트 박스 인 경우 테스트 대상 시스템 (SUT)을 리팩터링 할 때 일부 테스트 방법 이 취약해질 수 있습니다 .

모의에 호출 된 메소드 의 순서 를 확인하는 모의 프레임 워크를 사용하는 경우 (통화에 부작용이 없으므로 순서가 관련이없는 경우); 그런 다음 코드가 다른 순서로 메소드 호출로 깨끗하고 리팩터링되면 테스트가 중단됩니다. 일반적으로 모의는 테스트에 취약성을 유발할 수 있습니다.

개인 또는 보호 된 구성원을 노출시켜 SUT의 내부 상태를 확인하는 경우 (Visual Basic에서 "friend"를 사용하거나 액세스 수준을 "internal"로 에스컬레이션하고 c #에서 "internalsvisibleto"를 사용할 수 있음) c # " test-specific-subclass "를 사용할 수 있습니다.) 갑자기 클래스의 내부 상태가 중요합니다. 클래스를 블랙 박스로 리팩토링 할 수 있지만 화이트 박스 테스트는 실패합니다. SUT 상태가 변경 될 때 단일 필드가 다른 것을 의미하기 위해 재사용된다고 가정합니다 (좋은 방법은 아닙니다!). 두 필드로 나누면 깨진 테스트를 다시 작성해야 할 수도 있습니다.

테스트 특정 서브 클래스를 사용하여 보호 된 메소드를 테스트 할 수도 있습니다. 이는 프로덕션 코드 관점에서 리 팩터가 테스트 코드 관점에서 근본적인 변화임을 의미 할 수 있습니다. 보호 된 방법으로 또는 밖으로 몇 줄을 이동하면 생산 부작용이 없지만 테스트를 중단 할 수 있습니다.

" 테스트 후크 "또는 다른 테스트 별 또는 조건부 컴파일 코드를 사용하는 경우 내부 논리에 대한 취약한 종속성으로 인해 테스트가 중단되지 않도록하기가 어려울 수 있습니다.

따라서 테스트가 SUT의 친밀한 내부 세부 사항에 결합되는 것을 방지하려면 다음을 수행하는 데 도움이 될 수 있습니다.

  • 가능한 경우 모의보다는 스터브를 사용하십시오. 더 많은 정보를 참조 동어 반복적 시험에 파비오 Periera의 블로그동어 반복적 테스트에 내 블로그를 .
  • 모의를 사용하는 경우 중요하지 않은 한 호출되는 메소드의 순서를 확인하지 마십시오.
  • SUT의 내부 상태 확인을 피하십시오. 가능하면 외부 API를 사용하십시오.
  • 프로덕션 코드에서 테스트 별 논리를 피하십시오
  • 테스트 특정 서브 클래스를 사용하지 마십시오.

위의 모든 사항은 테스트에 사용 된 화이트 박스 커플 링의 예입니다. 리팩토링 차단 테스트를 완전히 피하려면 SUT의 블랙 박스 테스트를 사용하십시오.

면책 조항 : 여기서 리팩토링을 논의하기 위해 외부 영향없이 내부 구현을 변경하는 것을 포함하기 위해 단어를 조금 더 광범위하게 사용하고 있습니다. 일부 순수 주의자들은 원자 적 리팩토링 작업을 설명하는 Martin Fowler와 Kent Beck의 책 리팩토링에 동의하지 않을 수도 있습니다.

실제로, 우리는 여기에 설명 된 원 자성 연산보다 약간 큰 비 차단 단계를 취하는 경향이 있으며, 특히 생산 코드가 외부에서 동일하게 작동하도록하는 변경은 테스트를 통과하지 못할 수 있습니다. 그러나 리 팩터로 "동일한 동작을 가진 다른 알고리즘의 대체 알고리즘"을 포함시키는 것이 공정하다고 생각하며 Fowler도 동의합니다. Martin Fowler 자신은 리팩토링이 테스트를 중단 할 수 있다고 말합니다.

모의 테스트를 작성할 때 SUT가 아웃 바운드 통화를 테스트하여 공급 업체와 제대로 통신하는지 확인합니다. 고전적인 테스트는 최종 상태에만 신경을 씁니다. 따라서 Mockist 테스트는 메소드 구현과 더 관련이 있습니다. 공동 작업자 호출의 특성을 변경하면 일반적으로 모의 테스트가 중단됩니다.

[...]

구현 변경 사항은 리팩토링을 방해합니다. 구현 변경은 기존 테스트보다 테스트를 중단 할 가능성이 훨씬 높기 때문입니다.

Fowler- Mocks는 스텁이 아닙니다


파울러는 문자 그대로 리팩토링에 관한 책을 썼습니다. 단위 테스트 (Gerard Meszaros의 xUnit 테스트 패턴)에 대한 가장 권위있는 책은 Fowler의 "서명"시리즈에 있으므로 리팩토링이 테스트를 중단 할 수 있다고 말했을 때 아마도 맞을 것입니다.
완벽 주의자

5

리팩토링 할 때 테스트가 중단되면 리팩토링이 아닙니다. "리팩터링"은 프로그램의 동작을 변경하지 않고 프로그램의 구조를 변경하는 것입니다.

때로는 테스트 동작을 변경해야합니다. 아마도 두 개의 메소드 (예 : 수신 TCP 소켓 클래스의 bind () 및 listen ())를 함께 병합해야하므로 코드의 다른 부분에서 현재 변경된 API를 사용하지 못하고 실패합니다. 그러나 그것은 리팩토링이 아닙니다!


테스트에서 테스트 한 메소드의 이름 만 변경하면 어떻게 되나요? 테스트에서 이름을 바꾸지 않으면 테스트가 실패합니다. 여기서 그는 프로그램의 동작을 바꾸지 않습니다.
Oscar Mederos

2
이 경우 그의 테스트도 리팩토링됩니다. 그래도 조심해야합니다. 먼저 메소드의 이름을 바꾼 다음 테스트를 실행하십시오. 올바른 이유로 실패해야합니다 (컴파일 할 수 없음 (C #), MessageNotUnderstood 예외 (Smalltalk), 아무것도 발생하지 않는 것 (Objective-C의 null-eating pattern)). 그런 다음 실수로 버그가 발생하지 않았다는 것을 알고 테스트를 변경합니다. "테스트가 중단 된 경우"는 "리팩토링을 마친 후에 테스트가 중단 된 경우"를 의미합니다. 변경 청크를 작게 유지하십시오!
Frank Shearar

1
단위 테스트는 기본적으로 코드 구조와 연결됩니다. 예를 들어 Fowler는 refactoring.com/catalog 에 단위 테스트 (예 : 메소드 숨기기, 인라인 메소드, 오류 코드를 예외로 대체 등)에 영향을주는 많은 요소가 있습니다.
Kristian H

그릇된. 두 방법을 함께 병합하는 것은 공식 이름을 가진 리팩토링입니다 (예 : 인라인 방법 리팩토링이 정의에 적합 함). 인라인 된 방법의 테스트를 중단합니다. 이제 일부 테스트 사례를 다른 방법으로 다시 작성 / 테스트해야합니다. 단위 테스트를 중단하기 위해 프로그램의 동작을 변경할 필요는 없습니다. 단위 테스트가 포함 된 내부 구조를 재구성하기 만하면됩니다. 프로그램의 동작이 변경되지 않는 한 이것은 여전히 ​​리팩토링의 정의에 적합합니다.
KolA

나는 잘 작성된 테스트를 가정하여 위의 내용을 작성했습니다. 구현을 테스트하는 경우-테스트 구조가 테스트중인 코드의 내부를 반영하는 경우 확실합니다. 이 경우 구현이 아닌 유닛의 계약을 테스트하십시오.
Frank Shearar

4

이 질문의 문제점은 다른 사람들이 '리팩토링'이라는 단어를 다르게 취하고 있다는 것입니다. 나는 당신이 아마 몇 가지 의미를 신중하게 정의하는 것이 가장 좋다고 생각합니다.

>  Keep the API the same, but change how the API is implemented internally
>  Change the API

다른 사람이 이미 언급했듯이 API를 동일하게 유지하고 모든 회귀 테스트가 공개 API에서 작동하는 경우 아무런 문제가 없습니다. 리팩토링은 전혀 문제를 일으키지 않아야합니다. 실패한 테스트는 이전 코드에 버그가 있고 테스트가 잘못되었거나 새 코드에 버그가 있음을 의미합니다.

그러나 그것은 명백합니다. 따라서 리팩토링이란 API를 변경한다는 의미 일 것입니다.

어떻게 접근해야하는지 대답하겠습니다!

  • 먼저 새로운 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를 수정하는 단계적 프로세스를 통해 한동안 새롭고 오래된 코드가 공존 할 수 있습니다.


SUT에 대한 단위 테스트가 게시 된 Api에 대한 외부 클라이언트와 동일하다는 것이 분명하기 때문에이 답변이 마음에 듭니다. 처방 한 것은 '종속성 지옥'을 피하기 위해 게시 된 라이브러리 / 구성 요소를 관리하는 SemVer 프로토콜과 매우 유사합니다. 그러나 이것은 시간과 유연성이 필요하기 때문에 모든 마이크로 유닛의 공용 인터페이스에이 접근 방식을 추정하면 비용을 추정 할 수 있습니다. 보다 유연한 접근 방식은 가능한 한 테스트와 구현 테스트를 분리하는 것입니다. 즉 테스트 입력 및 출력을 설명하기 위해 통합 테스트 또는 별도의 DSL
KolA

1

나는 당신의 단위 테스트가 내가 "멍청한 것"이라고 부르는 세분성이라고 가정합니다. 즉, 그들은 각 클래스와 함수의 절대적인 정밀성을 테스트합니다. 코드 생성기 도구에서 벗어나 더 큰 표면에 적용되는 테스트를 작성하면 응용 프로그램의 인터페이스가 변경되지 않았으며 테스트가 여전히 작동한다는 것을 알고 원하는만큼 내부를 리팩터링 할 수 있습니다.

각각의 모든 방법을 테스트하는 단위 테스트를 원한다면 동시에 리팩토링해야합니다.


1
실제로 문제를 해결하는 가장 유용한 답변-내부 퀴즈의 흔들리는 기초에 테스트 범위를 만들거나 지속적으로 분리되지 않을 것으로 예상하지만 TDD는 정확히 반대의 행동을 취하기 때문에 가장 많이 하향 조정됩니다. 이것이 과대 광고 방식에 대한 불편한 진실을 지적하기 위해 얻는 것입니다.
KolA

1

리팩토링 중 및 리팩토링 후 테스트 스위트를 코드베이스와 동기화 상태로 유지

어려운 점은 커플 링 입니다. 모든 테스트는 구현 세부 사항에 어느 정도 결합되어 있지만 단위 테스트 (TDD 여부에 관계없이)는 내부를 방해하기 때문에 특히 나쁩니다. 단위 테스트가 많을수록 단위에 연결된 코드가 더 많습니다 (예 : 메소드 서명 / 다른 공용 인터페이스) 단위의-최소한.

정의에 따른 "단위"는 하위 수준의 구현 세부 사항이며, 단위의 인터페이스는 시스템이 진화함에 따라 변경 / 분할 / 병합되거나 변경 될 수 있습니다. 풍부한 단위 테스트는 실제로이 진화를 도움보다 더 방해 할 수 있습니다.

리팩토링시 테스트 중단을 피하는 방법은 무엇입니까? 커플 링을 피하십시오. 실제로 이는 가능한 한 많은 단위 테스트를 피하고 구현 세부 사항에 대해 더 높은 수준의 통합 테스트를 선호합니다. 실버 글 머리 기호는 없지만 테스트는 여전히 어떤 수준에서 무언가와 연결되어야하지만 이상적으로는 시맨틱 버전 관리를 사용하여 명시 적으로 버전이 지정된 인터페이스 여야합니다 (예 : 일반적으로 게시 된 API / 응용 프로그램 수준) 솔루션의 모든 단일 단위에 대해).


0

테스트가 구현과 너무 밀접하게 연결되어 있으며 요구 사항이 아닙니다.

다음과 같은 주석으로 테스트를 작성해보십시오.

//given something
...test code...
//and something else
...test code...
//when something happens
...test code...
//then the state should be...
...test code...

이런 식으로 테스트에서 의미를 리팩터링 할 수 없습니다.

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