테스트 가능한 코드 작성
게시 됨: 2022-11-03단위 테스트는 모든 소프트웨어 개발자의 도구 상자에 있는 필수 도구입니다. 최고의 소프트웨어 설계 사례, 패턴 및 원칙을 따르는 코드베이스를 다룰 때 단위 테스트를 작성하는 것은 상대적으로 쉽습니다. 진짜 문제는 잘못 설계되고 테스트할 수 없는 코드를 단위 테스트하려고 할 때 발생합니다.
이 블로그에서는 더 많은 단위 테스트 가능한 코드를 작성하는 방법과 테스트 가능성을 개선하기 위해 피해야 할 패턴 및 나쁜 관행에 대해 설명합니다.
테스트 가능 및 테스트 불가능 코드
장기적으로 유지 관리해야 하는 대규모 응용 프로그램에서 작업할 때 시스템의 전반적인 품질을 높게 유지하려면 자동화된 테스트에 의존해야 합니다. 여러 단위를 전체적으로 테스트하는 통합 테스트와 비교할 때 단위 테스트는 빠르고 안정적인 이점이 있습니다. 이상적으로는 테스트 중인 클래스만 인스턴스화하기 때문에 빠르며 일반적으로 데이터베이스 또는 네트워크 연결과 같은 외부 종속성을 조롱하기 때문에 안정적입니다.
단위 테스트와 통합 테스트의 정확한 차이점에 대해 잘 모르는 경우 테스트 소개 블로그에서 이 주제에 대해 자세히 알아볼 수 있습니다.
테스트 가능한 코드는 나머지 코드베이스와 분리될 수 있습니다. 즉, 가장 작은 단위를 독립적으로 테스트할 수 있습니다. 테스트할 수 없는 코드는 좋은 단위 테스트를 작성하는 것이 어렵거나 심지어 불가능한 방식으로 작성됩니다.
테스트 가능한 코드를 작성할 때 피해야 하는 몇 가지 안티 패턴과 나쁜 관행을 검토해 보겠습니다.
예제는 Java로 작성되었지만 여기에 언급된 코딩 규칙은 모든 객체 지향 프로그래밍 언어 및 테스트 프레임워크에 적용됩니다. 이 블로그 게시물의 예제로 assertj 및 JUnit5를 사용할 것입니다.
의존성 주입
종속성 주입은 테스트 격리를 달성하기 위한 가장 중요한 디자인 패턴 중 하나입니다. 의존성 주입은 한 객체가 스스로 생성하지 않고 생성자 매개변수나 설정자를 통해 다른 객체(종속성)를 받는 디자인 패턴입니다.
종속성 주입을 사용하면 개체의 종속성을 조롱하여 테스트 중인 클래스를 쉽게 격리할 수 있습니다.
종속성 주입이 없는 예를 살펴보겠습니다.

Car 클래스의 생성자에서 엔진 종속성이 생성되기 때문에 Car 클래스와 Engine 클래스가 밀접하게 결합되어 있다고 말할 수 있습니다. 그들은 서로에 대한 의존도가 높기 때문에 하나를 변경하려면 다른 하나를 변경해야 합니다.
테스트 관점에서 보면 위의 예제가 구체적인 Engine 구현을 테스트 이중으로 대체할 수 없기 때문에 Car 클래스를 개별적으로 테스트할 수 없습니다.
그러나 의존성 주입과 다형성을 사용하여 격리를 달성할 수 있습니다.

이제 여러 엔진 구현을 구성할 수 있으므로 엔진이 서로 다른 자동차를 구성할 수 있습니다.

Engine 추상화의 모의 구현을 만들고 이를 Car 클래스에 전달할 수 있기 때문에 이제 격리된 테스트가 가능합니다.

다른 개체(종속성)를 필요로 하는 개체를 처리할 때 이상적으로는 일부 추상화 뒤에 숨겨진 생성자 매개변수(종속성 주입)를 통해 제공해야 합니다.
이 패턴을 따르면 코드가 더 읽기 쉽고 시간이 지남에 따라 변경 사항에 적응할 수 있습니다. 또한 생성자에서 실제 작업을 수행하는 것을 피해야 합니다. 필드 할당 이상은 실제 작업입니다. 생성자의 `new` 키워드는 항상 테스트할 수 없는 코드의 경고 신호입니다.
여기서 주목해야 할 한 가지는 어떤 상황에서는 긴밀한 결합(예: 생성자의 새 키워드, 논리 격리를 위한 내부 클래스, 개체 매퍼)이 나쁜 습관이 아니라는 것입니다. 클래스는 "독립형" 클래스로 이해되지 않습니다.
전역 상태
전역 상태를 공유하면 특히 다중 스레드 환경에서 불안정한 테스트(때로는 통과, 때로는 실패)를 생성할 수 있습니다.
테스트 중인 여러 개체가 동일한 전역 상태를 공유하는 시나리오를 상상해 보십시오. 개체 중 하나의 메서드가 공유 전역 상태의 값을 변경하는 부작용을 트리거하면 다른 개체의 메서드 출력을 예측할 수 없게 됩니다. 불순한 정적 메서드는 어떤 방식으로든 전역 상태를 변경하거나 일부 전역 상태에 프록시되므로 사용하지 마십시오.
이 순수하지 않은 정적 메서드를 살펴보겠습니다.

기본적으로 이 메서드는 현재 시스템 날짜와 시간을 읽고 해당 값을 기반으로 결과를 반환합니다. LocalDateTime.now() 정적 호출이 테스트를 실행하는 동안 다른 결과를 생성하기 때문에 이 메서드에 대한 적절한 상태 기반 단위 테스트를 작성하는 것은 매우 어려울 것입니다. 이 방법에 대한 테스트 작성은 시스템 날짜와 시간을 변경하지 않고는 불가능합니다.
이 문제를 해결하기 위해 날짜 시간을 timeOfDay 메서드에 인수로 전달합니다.


timeOfDay 정적 메서드는 이제 순수합니다. 동일한 입력은 항상 동일한 결과를 생성합니다. 이제 테스트에서 분리된 dateTime 객체를 인수로 쉽게 전달할 수 있습니다.

데메테르의 법칙
데메테르의 법칙(Law of Demeter) 또는 최소 지식의 원칙은 객체는 첫 번째 객체와 밀접하게 관련된 객체에 대해서만 알아야 한다고 말합니다. 즉, 하나의 개체는 필요한 개체에만 액세스할 수 있어야 합니다. 예를 들어 컨텍스트 객체를 인수로 받아들이는 메서드가 있습니다.

이 방법은 작업을 수행하는 데 필요한 정보를 얻기 위해 개체 그래프를 걸어야 하기 때문에 데메테르의 법칙을 위반합니다. 클래스와 메서드에 불필요한 정보를 전달하면 테스트 가능성이 떨어집니다.
다른 객체에 대한 참조를 포함하는 거대한 BillingContext 객체를 상상해 보십시오.

보시다시피 테스트는 불필요한 정보로 가득 차 있습니다. 복잡한 개체 그래프를 생성하는 테스트는 읽기 어렵고 불필요한 복잡성을 유발합니다.
이전 예제를 수정해 보겠습니다.

항상 직접 종속성을 클래스와 메서드에 전달해야 합니다. 그러나 많은 인수를 메서드에 전달하는 것도 좋은 방법이 아닙니다. 이상적으로는 최대 두 개의 인수를 전달하거나 가까운 관련 인수를 데이터 개체에 래핑해야 합니다.
신의 물건
God 객체는 다른 많은 개별 객체를 참조하고, 하나 이상의 책임이 있으며, 변경해야 하는 여러 이유가 있는 객체입니다. 학급에서 하는 일을 요약하는 것이 어렵거나 요약에 "and"라는 단어가 포함된 경우 학급에는 둘 이상의 책임이 있을 수 있습니다.
God 객체는 관련되지 않은 여러 종속성을 다루기 때문에 테스트하기 어렵습니다. 다양한 수준의 추상화와 우려가 혼합되어 있고 많은 부작용이 발생합니다. 결과적으로 테스트 사례에 대해 원하는 상태를 달성하기가 어렵습니다.
예를 들어:

UserService는 새 사용자를 등록하고 이메일을 보내는 두 가지 이상의 책임이 있습니다. 사용자 등록을 테스트하는 동안 이메일 서비스를 처리해야 하며 그 반대의 경우도 마찬가지입니다.

관련되지 않은 종속성이 2개 이상 있는 UserService를 상상해 보십시오. 이러한 종속성에는 고유한 종속성이 있습니다. 우리는 읽을 수 없고 관련 없는 정보로 가득 차 있고 이해하기 매우 어려운 테스트로 끝날 것입니다. 따라서 모든 클래스는 단 하나의 책임과 변경 사유를 가져야 합니다. 변경해야 할 이유가 하나뿐인 클래스는 단일 책임 원칙이라는 5가지 소프트웨어 설계 원칙 중 하나입니다.
여기에서 SOLID 원칙에 대해 자세히 알아볼 수 있습니다.
결론
소프트웨어 설계 모범 사례를 따르는 코드베이스는 단위 테스트 작성을 훨씬 더 관리하기 쉽게 만듭니다.
반면에 언급된 안티 패턴을 사용하여 코드베이스에 대한 작업 단위 테스트를 작성하는 것은 매우 어렵거나 때로는 불가능할 수도 있습니다. 좋은 테스트 가능한 코드를 작성하려면 많은 연습, 훈련 및 추가 노력이 필요합니다. 테스트 가능한 코드의 가장 중요한 이점은 테스트 용이성과 해당 코드를 이해, 유지 관리 및 확장할 수 있는 능력입니다.
이 블로그가 테스트 가능한 코드를 작성하는 데 도움이 되기를 바랍니다.