코드의 치환
테스트 하기 편리한 프로그램을 완성하는 방법은 개발과정에서 테스트 루틴을 작성하거나 테스트를 지원하는 설계를 위해 시간을 투자하는 방법입니다.
사실 코드를 작성할 때 테스트하기 편리한 방향으로 설계를 진행하다보면 자연스럽게 객체지향적인 코드를 작성하기 좋게 됩니다. 저도 이런저런 테스트코드를 작성하면서 mock 객체를 사용하기 위해 기존의 클래스가 제공하는 기능을 자연스럽게 분리하게 되고 그로 인해 OCP나 IOC 같은 객체지향에서 지향해야 할 방향으로 코드를 작성하게 됩니다.
치환
테스트 코드를 디테일하게 체크하기 위해서는 해당 코드가 잘 실행 되는지 검증할 필요가 있습니다.
검증을 위해 기존의 테스트코드 내에서 테스트가 불필요한 부분을 제외할 필요가 있습니다. 이를 위해 여러가지 방식을 통해 불필요한 부분을 테스트 가능하게 치환 할 수 있습니다.
1. 전처리 치환
preprocessing 은 컴파일러가 코드를 컴파일 하기 전에 코드를 미리 한번 처리하는 작업입니다. 이 과정에서 매크로를 치환하고, 컴파일 가능 여부를 확인하고, 그밖에 타 코드의 include 를 진행하게 되는데, 여기서 매크로를 치환 하는 것을 통해 우리가 테스트에 필요한 코드를 주입할 수 있습니다. 일반적으로 c/c++ 에서 사용할 수 있는 방법 입니다.
1 | // test.c |
1 | // testdefs.h |
위 코드에서는 #ifdef
, #define
을 이용하여 테스트용 컴파일에서는 실제 need_injection이 아닌 매크로 함수로 정의된 테스트용 로직을 사용하게 합니다. 이제 테스트코드를 실행 한 후에 전역으로 선언된 test_param라는 전역변수를 통해 정말 전달된 파라메터 + 10
한 값이 저장되어 있는지 확인 하여 실제로 원하는대로 실행되었는지 확인합니다.
이런 코드의 치환에서 중요한 점은 원래 코드의 흐름을 테스트를 위해 바꾸지 말아야한다는 점입니다.
2. 링크 치환
링크는 컴파일러가 preprocessing 이후 코드를 중간 언어를 생성한 후 c/c++ 의 경우 링커를 통해(자바의 경우 컴파일러) 각 중간언어로 치환된 코드들을 조합하는 과정을 거칩니다. 링크 치환은 이 링크 과정에서 원래 코드를 테스트용 코드로 치환합니다.
자바를 예를 들면
1 |
|
1 |
|
위 코드에서 Test 라는 클래스는 MustInjection 이라는 클래스에 대해 의존성을 가지고 있습니다. 그리고 이 클래스는 io.github.ksmail13.test
라는 패키지에 있습니다. 링크 치환을 통해 해당 클래스를 변경하고 싶다면, 테스트용 classpath에 원본 클래스가 있는 패키지와 동일한 경로의 패키지를 별도의 생성하여 테스트를 위한 모조 클래스를 작성해두고 테스트 할 때 테스트용 classpath를 사용하도록 지정합니다.
테스트용 컴파일 과정에서 컴파일용 개별 빌드 스크립트를 작성하여 테스트용으로 작성한 코드가 테스트 대상 클래스에 링크를 할 수 있게 설정하고, 테스트 런타임에서 치환된 테스트클래스를 통해 테스트 진행 합니다. 어느정도 수고스러운 작업이기는 하지만 여러 위치에서 특정 라이브러리에 대해 의존성을 가지고 있는 경우 쉽게 대체 할 수 있다는 장점이 있습니다.
또한 사실상 같은 인터페이스를 가지는 코드를 제작하는 것이기 때문에, 반환이 많은 함수/메소드를 가진 라이브러러 보다는 프로시저 형태의 라이브러리에서 사용하는 경우에 더 유용하게 사용할 수 있습니다.
3. 객체 치환
객체 치환은 객체지향 패러다임을 가진 언어에서 사용할 수 있는 방식입니다. 의존성을 가지는 부분을 별도의 클래스 혹은 인터페이스로 분리하여 테스트용 객체로 치환 할 수 있는 형태의 치환 방법입니다. 일반적으로 mockito 같은 mock 객체와 연동하는 과정에서 가장 쉽게 사용하는 방식의 치환 방식입니다.
클래스의 구조를 의존성에 맞게 설계해야 하며 이 과정에서 클래스의 구조가 깔끔해진다는 장점이 있습니다. 또한 코드 자체가 테스트하기에 용이한 코드로 작성되기 때문에 자바의 경우에는 요즘에 주로 사용하는 maven, gradle 환경에서 별다른 추가 설정없이 코드를 치환할 수 있다는 장점이 있습니다.
1 |
|
1 |
|
첫번째 코드는 테스트 대상 클래스이고 두번째는 해당 클래스를 테스트 하는 코드 입니다. TestTarget 클래스는 MustInjection 이라는 인터페이스에 대해 의존성을 가지고 있고 test 코드에서 mock 객체를 만들어 TestTarget에 주입하여 테스트를 진행했습니다.
여기서는 mockito 같은 mocking 라이브러리를 사용하여 작업했지만 사용환경에 따라 원래 클래스를 상속하건 인터페이스를 구현한 테스트용 가짜 객체를 주입하는 방식으로도 구현이 가능합니다.
출처
이 글은 레거시 활용 전략의 4장의 내용을 읽고 제 나름대로 재구성했습니다.