리팩토링 할 코드에 대한 테스트를 작성해야하는 이유는 무엇입니까?


15

거대한 레거시 코드 클래스를 리팩토링하고 있습니다. 리팩토링 (나는 추정한다)은 이것을 옹호한다 :

  1. 레거시 클래스에 대한 테스트 작성
  2. 수업에서 도덕을 리팩터링하다

문제 : 클래스를 리팩터링하면 1 단계의 테스트를 변경해야합니다. 예를 들어, 레거시 메서드에 있던 것이 이제는 별도의 클래스가 될 수 있습니다. 한 가지 방법은 여러 가지 방법 일 수 있습니다. 레거시 클래스의 전체 환경은 새로운 것으로 사라질 수 있으므로 1 단계에서 작성한 테스트는 거의 무효가됩니다. 본질적으로 3 단계를 추가 할 것입니다. 테스트를 적절 하게 다시 작성하십시오.

리팩토링 전에 테스트를 작성하는 목적은 무엇입니까? 그것은 나 자신을 위해 더 많은 일을 만들어내는 학문적 운동처럼 들린다. 나는 지금 방법에 대한 테스트를 작성하고 있으며 물건을 테스트하는 방법과 레거시 방법이 작동하는 방법에 대해 더 많이 배우고 있습니다. 레거시 코드 자체를 읽음으로써 이것을 배울 수 있지만, 테스트를 작성하는 것은 코를 문지르는 것과 거의 동시에이 임시 지식을 별도의 테스트로 문서화하는 것과 같습니다. 따라서이 방법으로 코드의 기능을 배우는 것 외에는 선택의 여지가 거의 없습니다. 나는 코드에서 지옥을 리팩터링 할 것이고 여기에서 나의 지식이 유지되고 리팩토링에 대해 더 신선하게하는 것을 제외하고는 모든 문서와 테스트가 중요하지 않은 경우 무효가 될 것이기 때문에 나는 여기서 임시로 말했다.

리 팩터 전에 테스트를 작성해야하는 실제 이유가 있습니까? 코드를 더 잘 이해하는 데 도움이됩니까? 또 다른 이유가 있습니다!

설명 해주십시오!

노트 :

이 게시물이 있습니다 : 완전한 리팩토링 시간이 없을 때 레거시 코드에 대한 테스트를 작성하는 것이 합리적입니까? "리팩토링 전에 테스트 쓰기"라고 말하지만 "이유"또는 "쓰기 테스트"가 "곧 폐기 될 바쁜 작업"처럼 보일 경우 수행 할 조치는 말하지 않습니다.


1
전제가 잘못되었습니다. 테스트는 변경하지 않습니다. 새로운 테스트를 작성합니다. 3 단계는 "현재 중단 된 테스트는 모두 삭제"입니다.
pdr

1
그런 다음 3 단계는 "새 테스트 작성. 기능 테스트 삭제"를 읽을 수 있습니다. 나는 그것이 여전히 원래의 작품을 파괴하는 것이라고 생각합니다
Dennis

3
아니요, 2 단계에서 새 테스트를 작성하려고합니다. 1 단계가 손상되었습니다. 하지만 시간 낭비였습니까? 아닙니다. 2 단계에서 아무 것도 깨뜨리지 않는다는 확신을 심어주기 때문에 새로운 테스트에서는 효과가 없습니다.
pdr

3
@Dennis-상황과 관련하여 동일한 우려 사항을 많이 공유하지만 대부분의 리팩토링 노력을 "원래 작업 파괴"로 간주 할 수 있지만 파괴하지 않으면 10k 라인의 스파게티 코드에서 한 번에 이동하지 않습니다. 파일. 아마도 단위 테스트를 위해 가야 할 것이고, 테스트하고있는 코드와 함께 진행됩니다. 코드가 발전하고 사물이 이동 및 / 또는 삭제되면 단위 테스트도 함께 진화해야합니다.
DXM

"코드 이해"는 작은 이점이 아닙니다. 이해하지 못하는 프로그램을 어떻게 리팩터링 할 것으로 기대하십니까? 철저한 테스트를 작성하는 것보다 프로그램에 대한 진정한 이해를 보여주는 더 좋은 방법은 불가피합니다. 또한 테스트가 더 추상적 일수록 나중에 스크래치해야 할 가능성이 적으므로 처음에는 높은 수준의 테스트를 수행해야합니다.
Neil

답변:


46

리팩토링은 (외부 적으로 보이는) 동작을 변경하지 않고 코드 조각 (예 : 스타일, 디자인 또는 알고리즘 개선)을 정리합니다. 리팩토링 전후의 코드 동일한 지 확인하기 위해 테스트를 작성하는 대신 리팩토링 전후의 애플리케이션 이 동일하게 작동 하는 지표로 테스트를 작성하십시오 . 새 코드가 호환되며 새로운 버그가 도입되지 않았습니다.

주요 관심사는 소프트웨어의 공용 인터페이스에 대한 단위 테스트를 작성하는 것입니다. 이 인터페이스는 변경되지 않아야하므로 테스트 (이 인터페이스에 대한 자동 검사)도 변경되지 않아야합니다.

그러나 테스트는 오류를 찾는 데 유용하므로 소프트웨어의 개인 부분에 대한 테스트도 작성하는 것이 좋습니다. 이러한 테스트는 리팩토링 전체에서 변경 될 것으로 예상됩니다. 구현 세부 사항 (예 : 개인 함수의 이름 지정)을 변경하려면 먼저 변경된 예상을 반영하도록 테스트를 업데이트 한 다음 테스트가 실패하는지 확인하십시오 (예상이 충족되지 않은 경우). 실제 코드를 변경하십시오. 모든 테스트가 다시 통과되는지 확인하십시오. 퍼블릭 인터페이스 테스트가 실패하지 않아야합니다.

더 큰 규모로 변경을 수행하는 경우 (예 : 여러 개의 종속 부품 재 설계) 더 어렵습니다. 그러나 어떤 종류의 경계가 있으며, 그 경계에서 테스트를 작성할 수 있습니다.


6
+1. 내 마음을 읽고 내 대답을 썼습니다. 중요한 점 : 리팩토링 후에도 여전히 동일한 버그가 있음을 나타 내기 위해 단위 테스트를 작성해야 할 수도 있습니다!
david.pfx

질문 : 함수 이름 변경 예제에서 왜 테스트가 실패했는지 확인하기 위해 테스트를 변경합니까? 물론 변경하면 실패 할 것입니다. 링커가 코드를 묶는 데 사용하는 연결이 끊어졌습니다! 방금 새로 선택한 이름으로 다른 기존의 개인 기능이있을 것으로 예상하고 있고, 놓친 경우가 아닌지 확인해야합니까? 나는 이것이 당신에게 OCD와의 경계에 대한 확실한 확신을 줄 것이지만,이 경우 그것은 마치 과잉 같은 느낌입니다. 예제의 테스트가 실패하지 않는 가능한 이유가 있습니까?
Dennis

^ cont : 일반적인 기술로서 가능한 한 빨리 잘못된 일을 포착하기 위해 코드의 단계별 위생 검사를 수행하는 것이 좋습니다. 매번 손을 씻지 않으면 몸이 아프지 않을 수 있지만, 단순히 손을 습관으로 씻으면 오염 된 물건에 닿았는지 여부에 관계없이 전반적인 건강을 유지할 수 있습니다. 여기에서는 손을 불필요하게 씻거나 때로는 불필요하게 코드를 테스트 할 수 있지만, 코드를 건강하게 유지하는 데 도움이됩니다. 그게 당신의 요점입니까?
Dennis

@Dennis는 실제로 과학적으로 올바른 실험을 무의식적으로 묘사했습니다. 하나의 매개 변수를 더 많이 변경할 때 실제로 어떤 매개 변수가 결과에 영향을 미치는지 알 수 없습니다. 테스트는 코드이며 모든 코드에는 버그가 있습니다. 코드를 터치하기 전에 테스트를 실행하지 않은 프로그래머 지옥에 갈 것입니까? 반드시 그렇지는 않습니다. 테스트를 실행하는 것이 이상적이지만 그것이 필요한지 여부는 전문가의 판단입니다. 또한 컴파일되지 않으면 테스트가 실패했으며 내 대답은 링커가있는 정적 언어뿐만 아니라 동적 언어에도 적용 할 수 있습니다.
amon

2
리팩토링하는 동안 다양한 오류를 수정하여 테스트하지 않고 쉽게 코드 이동을 수행하지 않았 음을 깨닫고 있습니다. 테스트는 코드를 변경하여 소개하는 행동 / 기능 "diffs"에 대해 경고합니다.
Dennis

7

아, 레거시 시스템을 유지합니다.

이상적으로 테스트는 나머지 코드베이스, 다른 시스템 및 / 또는 사용자 인터페이스와의 인터페이스를 통해서만 클래스를 처리합니다. 인터페이스. 업스트림 또는 다운 스트림 구성 요소에 영향을주지 않으면 서 인터페이스를 리팩터링 할 수 없습니다. 그것이 하나의 밀접하게 결합 된 엉망이라면 리팩토링보다는 재 작성 노력을 고려할 수도 있지만 의미 론적입니다.

편집 : 코드의 일부가 무언가를 측정하고 단순히 값을 반환하는 함수가 있다고 가정 해 봅시다. 유일한 인터페이스는 함수 / 메소드 / whatnot을 호출하고 반환 된 값을받는 것입니다. 결합이 느슨하고 단위 테스트가 쉽습니다. 기본 프로그램에 버퍼를 관리하는 하위 구성 요소가 있고 모든 호출이 버퍼 자체, 일부 제어 변수에 따라 달라지며 다른 코드 섹션을 통해 오류 메시지를 걷어차면 밀접하게 연결되어 있다고 말할 수 있습니다. 단위 테스트가 어렵다. 여전히 충분한 양의 모의 객체로 할 수는 있지만 지저분합니다. 특히 c. 버퍼의 작동 방식을 리팩토링하면 하위 구성 요소가 손상됩니다.
편집 종료

안정적인 인터페이스를 통해 클래스를 테스트하는 경우 리팩토링 전후에 테스트가 유효해야합니다. 이를 통해 중단하지 않았다는 확신을 가지고 변경을 수행 할 수 있습니다. 적어도 더 많은 자신감.

또한 점진적으로 변경할 수 있습니다. 이것이 큰 프로젝트라면, 당신이 그것을 완전히 분해하고 새로운 시스템을 구축 한 다음 테스트 개발을 시작하고 싶지 않을 것입니다. 한 부분을 변경하고 테스트 한 후 변경으로 인해 나머지 시스템이 다운되지 않도록 할 수 있습니다. 또는 만약 그렇다면, 출시 할 때 놀라지 않고 거대한 엉킨 엉망진창이 생길 수 있습니다.

메소드를 세 개로 나눌 수 있지만 이전 메소드와 동일한 방식으로 수행되므로 이전 메소드에 대한 테스트를 수행하여 세 가지로 나눌 수 있습니다. 첫 번째 시험을 작성하는 노력은 낭비되지 않습니다.

또한 레거시 시스템의 지식을 "임시 지식"으로 취급하는 것은 좋지 않습니다. 레거시 시스템과 관련하여 이전에 어떻게 수행했는지 아는 것이 중요합니다. "도대체 왜 그렇게합니까?"라는 오래된 질문에 매우 유용합니다.


나는 이해한다고 생각하지만, 당신은 인터페이스에서 나를 잃었다. 즉, 내가 쓰고있는 테스트는 테스트중인 메소드를 호출 한 후 특정 변수가 올바르게 채워 졌는지 확인합니다. 해당 변수가 변경되거나 리팩토링되면 내 테스트도 진행됩니다. 내가 사용하고있는 기존 레거시 클래스에는 인터페이스 / getter / setter가 없으므로 변수를 변경하거나 작업 집약이 덜합니다. 그러나 레거시 코드와 관련하여 인터페이스가 무엇을 의미하는지 잘 모르겠습니다. 어쩌면 내가 만들 수 있습니까? 그러나 그것은 리팩토링이 될 것입니다.
Dennis

1
예, 모든 것을 수행하는 하나의 신 클래스가 있다면 실제로 인터페이스가 없습니다. 그러나 다른 클래스를 호출하면 최상위 클래스는 특정 방식으로 작동 할 것으로 예상하고 단위 테스트에서 그렇게 할 수 있습니다. 그래도 리팩토링 중에 단위 테스트를 업데이트하지 않아도 될 것 같지는 않습니다.
Philip

4

내 자신의 답변 / 실현 :

리팩토링하는 동안 다양한 오류를 수정하여 테스트하지 않고 코드 이동을 쉽게 수행하지 않았 음을 깨닫고 있습니다. 테스트는 코드를 변경하여 소개하는 행동 / 기능 "diffs"에 대해 경고합니다.

좋은 시험이있을 때 과도하게 인식 할 필요는 없습니다. 좀 더 편안한 태도로 코드를 편집 할 수 있습니다. 테스트는 검증 및 온 전성 검사를 수행합니다.

또한 테스트는 리팩토링하고 파괴되지 않은 것과 거의 동일하게 유지되었습니다. 실제로 코드에 대해 더 깊이 파고 들어 테스트에 어설 션을 추가 할 수있는 추가 기회를 발견했습니다.

최신 정보

글쎄, 이제 테스트를 많이 변경하고 있습니다 : / 원래 함수를 리팩토링했기 때문에 (함수를 제거하고 대신 새로운 클리너 클래스를 만들었습니다. 이전에 실행 한 테스트중인 코드는 다른 클래스 이름으로 다른 매개 변수를 취하고 다른 결과를 생성합니다 (플러 프가있는 원래 코드에는 테스트 할 결과가 더 많았습니다). 그리고 내 테스트는 이러한 변경 사항을 반영해야하며 기본적으로 테스트를 새로운 것으로 작성하고 있습니다.

테스트 재 작성을 피할 수있는 다른 솔루션이 있다고 가정합니다. 예를 들어 새 코드와 그 안에있는 플러 프를 사용하여 이전 함수 이름을 유지하십시오 ...하지만 그것이 최고의 아이디어인지 모르겠으며 아직해야 할 일에 대한 판단을 내릴만한 많은 경험이 없습니다.


리팩토링과 함께 애플리케이션을 재 설계 한 것처럼 들립니다.
JeffO

언제 리팩토링하고 언제 다시 디자인합니까? 즉, 리팩토링 할 때 더 큰 다루기 힘든 클래스를 더 작은 클래스로 나누지 않고 옮기기도 어렵습니다. 그렇습니다, 나는 그 차이점을 정확히 확신하지 못하지만 아마도 두 가지를 모두하고 있습니다.
Dennis

3

테스트를 사용하여 코드를 실행하십시오. 레거시 코드에서 이것은 변경하려는 코드에 대한 테스트를 작성하는 것을 의미합니다. 그렇게하면 별도의 인공물이 아닙니다. 테스트는 코드가 수행해야하는 내용에 대한 것이 아니라 코드가 수행해야하는 내용에 대한 것이어야합니다.

일반적으로 코드 동작이 예상대로 작동하는지 확인하기 위해 리팩토링하려는 코드에 대해 테스트가없는 코드에 대한 테스트를 추가하려고합니다. 따라서 리팩토링하는 동안 테스트 스위트를 지속적으로 실행하는 것은 환상적인 안전망입니다. 테스트 스위트없이 코드를 변경하여 변경 사항이 예상치 않은 것에 영향을 미치지 않는지 확인하는 것은 무섭습니다.

오래된 테스트 업데이트, 새로운 테스트 작성, 오래된 테스트 삭제 등의 핵심은 현대의 전문 소프트웨어 개발 비용의 일부로 볼 수 있습니다.


첫 번째 단락은 1 단계를 무시하고 테스트를 작성하는 것을 옹호하는 것으로 보입니다. 두 번째 단락은 모순되는 것으로 보입니다.
pdr

내 답변을 업데이트했습니다.
Michael Durrant

2

특정 사례에서 리팩토링의 목표는 무엇입니까?

우리 모두가 TDD (Test-Driven Development)를 어느 정도 믿는다 고 생각합니다.

리팩토링의 목적이 기존 동작을 변경하지 않고 기존 코드를 정리하는 것이라면 리팩토링 전에 테스트를 작성하는 것이 코드의 동작을 변경하지 않았는지 확인하는 방법입니다. 성공하면 테스트가 전후에 성공합니다 당신은 리팩토링합니다.

  • 테스트는 새 작업이 실제로 작동하는지 확인하는 데 도움이됩니다.

  • 테스트는 아마도 원래 작업이 작동하지 않는 경우도 발견 할 것입니다 .

그러나 행동에 어느 정도 영향을 미치지 않고 어떻게 중요한 리팩토링을 실제로 수행 합니까?

다음은 리팩토링 중에 발생할 수있는 몇 가지 간단한 목록입니다.

  • 변수 이름 바꾸기
  • 기능 명 변경
  • 기능 추가
  • 기능 삭제
  • 기능을 둘 이상의 기능으로 나누기
  • 둘 이상의 기능을 하나의 기능으로 결합
  • 스플릿 클래스
  • 수업을 결합
  • 클래스 이름 바꾸기

나는 나열된 활동 중 하나 하나가 어떤 방식 으로든 행동변화 시킨다고 주장 할 것입니다.

리팩토링이 동작을 변경하더라도 테스트는 여전히 아무것도 깨지지 않았는지 확인하는 방법입니다.

거동은 거시적 수준에서 변하지 않을 수도 있지만, 단위 테스트 의 요점은 거시적 거동을 보장하는 것이 아닙니다. 그건 통합 테스트. 단위 테스트의 요점은 제품을 구성하는 개별 비트와 조각이 손상되지 않았는지 확인하는 것입니다. 체인, 가장 약한 링크 등

이 시나리오는 어떻습니까 :

  • 당신이 가지고 있다고 가정 function bar()

  • function foo() 전화하다 bar()

  • function flee() 또한 함수를 호출합니다 bar()

  • 다양성 flam()을 위해foo()

  • 모든 것이 훌륭하게 작동합니다 (적어도 적어도).

  • 리팩터링 ...

  • bar() 로 이름이 변경되었습니다 barista()

  • flee() 전화로 변경 barista()

  • foo()되어 있지 통화로 변경barista()

물론, 모두를위한 당신의 검사 결과 foo()flam() 실패합니다.

어쩌면 당신은 미치지 않았다 foo()라고 bar()처음에. 당신은 그것이의 방법에 flam()의존 한다는 것을 확실히 몰랐습니다 .bar()foo()

도대체 무엇이. 요점은 당신의 검사 결과가 모두 새로 파괴 행위를 발견하는 것입니다 foo()그리고 flam(), 당신의 리팩토링 작업시 증분 방식으로.

테스트는 리팩토링에 도움이됩니다.

테스트가없는 한.

이것은 약간의 고안된 예입니다. 변화하는 경우에 있다고 주장하는 사람들이있다 bar()휴식을 foo(), 다음 foo()으로 시작하는 것이 너무 복잡하고 세분화해야한다. 그러나 절차는 다른 이유로 다른 절차를 호출 할 수 있으며 모든 복잡성 을 제거 하는 것이 불가능 합니까? 우리의 임무는 복잡성을 합리적으로 잘 관리하는 것입니다.

다른 시나리오를 고려하십시오.

당신은 건물을 건설하고 있습니다.

건물이 올바르게 건축되도록 비계를 건축하십시오.

비계를 사용하면 무엇보다도 엘리베이터 샤프트를 만들 수 있습니다. 그 후에 비계를 찢어 지지만 엘리베이터 샤프트는 남아 있습니다. 비계를 파괴하여 "원작"을 파괴했습니다.

비유는 미묘하지만 요점은 제품을 빌드하는 데 도움이되는 도구를 만드는 것이 결코 전례가 없다는 것입니다. 도구가 영구적이지 않더라도 유용합니다 (필요한 경우에도). 목수는 항상 하나의 직업에 대해서만 지그를 만듭니다. 그런 다음 지그를 분리하여 때로는 다른 작업을 위해 다른 지그를 만드는 데 부품을 사용합니다. 그러나 그것은 지그를 쓸모 없거나 낭비하는 노력으로 만들지 않습니다.

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