• 안티 패턴: 겉으로 적절한 것처럼 보이지만 장래에 더 큰 문제로 이어지는 반복적인 문제

비공개 메서드 단위 테스트

비공개 메서드와 테스트 취약성

  • 비공개 메서드를 노출하는 경우 식별할 수 있는 동작만 테스트하는 것을 위반한다.
  • 비공개 메서드를 노출하면 테스트가 구현 세부 사항과 결합되고 결과적으로 리팩터링 내성이 떨어진다.

비공개 메서드와 불필요한 커버리지

  • 비공개 메서드가 너무 복잡해서 식별할 수 있는 동작으로 테스트하기에 충분히 커버리지를 얻을 수 없는 경우가 있다. 이런 경우 두 가지 문제가 존재할 수 있다.
    • 죽은 코드다. 테스트에서 벗어난 코드가 어디에도 사용되지 않는다면 리팩터링 후에도 남아서 관계없는 코드일 수 있다. 이러한 코드는 삭제하는 것이 좋다.
    • 추상화가 누락돼 있다. 비공개 메서드가 너무 복잡하면 별도의 클래스로 도출해야 하는 추상화가 누락됐다는 징후다.

비공개 메서드 테스트가 타당한 경우

  • 비공개 메서드를 테스트하는 것 자체는 나쁘지 않다. 비공개 메서드가 구현 세부 사항의 프록시에 해당하므로 나쁜 것이다.
  • 비공개이면서 식별할 수 있는 동작(예: ORM의 비공개 생성자)인 경우 공개로 바꾼다고, 캡슐화가 깨지지 않는다.
    • 이런 경우 공개로 바꾸어서 테스트하면 된다.
  • 만약, API 노출 영역을 가능한한 작게 하려면 테스트에서 리플렉션을 통해 테스트할 수 있다.
    • 해킹처럼 보이지만, ORM을 따르고 있으며 배후에 리플렉션을 사용하기도 한다.

비공개 상태 노출

  • 비공개를 지켜야 하는 상태를 노출하지 말고 식별할 수 있는 동작만 테스트 해야한다.
  • 테스트 유의성을 위해 공개 API 노출 영역을 넓히는 것은 좋지 않은 관습이다.

테스트로 유출된 도메인 지식

  • 도메인 지식을 테스트로 유출하는 것은 안티 패턴이며, 보통 복잡한 알고리즘을 다루는 테스트에서 일어난다.
  • 아래 예시처럼 테스트에 제품 코드의 알고리즘을 그대로 복사하면 안된다.
    • 알고리즘을 복제하는 대신 예상 값을 하드 코딩해야 된다.
public static Calculator {

  public static int Add(int value1, int value2) {
       return value1 + value2
  }
}


public class CalculatorTests {

   public void Adding_two_numbers() {
       int value1 = 1;
       int value2 = 3;
       int expected = value1 + value2;  <-- 유출!! 
       
       int actual = Calculator.Add(value1, value2)
   
       Assert.Equal(expected, actual)
   }
}

코드 오염

  • 테스트에만 필요한 내용을 제품 코드에 추가하지 말라.
    • 예: 제품 코드에 테스트 환경인지 확인하고 분기 처리
    • 제품 코드에 테스트 환경인지 구분하는 boolean 값을 가지기 보다는 인터페이스를 도입해서 두 가지 구현을 생성하라
      • 테스트 환경에 필요한 구현체는 테스트 코드에 포함
  • 운영 목적으로 사용하지 않는 코드를 잘못 호출하는 일을 방지하기 위해서다.

구체 클래스를 목으로 처리하기

  • 일부 기능을 지키려고 구체 클래스를 목으로 처리해야 하면, 이는 단일 책임 원칙을 위반하는 결과다.
  • 험블 객체 디자인 패턴을 사용해서 책임을 분리하고, 외부 의존성이 있는 부분만 목으로 처리해라.

시간 처리하기

  • 애플리케이션 기능 중에 현재 날짜와 시간에 접근이 필요한 경우가 있다.
    • 시간에 따라 달라지는 기능을 테스트하면 거짓 양성이 발생할 수 있다.
  • 이 의존성을 안정화하는데 세 가지 방법이 있다.
    • 안티 패턴: 앰비언트 컨텍스트로서의 시간
    • 바람직한 방법: 명시적 의존성으로서의 시간 2가지

앰비언트 컨텍스트로서의 시간

  • 앰비언트 컨텍스트: 정적메서드를 사용한 주입
  • 앰비언트 컨텍스트는 안티패턴이다.
    • 제품 코드를 오염시키고 테스트를 더 어렵게 한다.
    • 정적 필드는 테스트 간에 공유하는 의존성을 도입해 해당 테스트를 통합 테스트 영역으로 전환한다.
object DateTimeServer {  
  
    private lateinit var function: () -> LocalDateTime  
    
    fun init(function: () -> LocalDateTime) {  
       this.function = function   
	}
	   
	fun now(): LocalDateTime {  
	       return function()  
	    }  
}

DateTimeServer.init { LocalDateTime.now() }  
DateTimeServer.init { LocalDateTime.of(2023, 11, 16, 16, 46) }

명시적 의존성으로서의 시간

  • 두 가지 방법
    • 서비스로 시간 의존성을 명시적으로 주입
    • 일반 값으로 시간 의존성을 명시적으로 주입
  • 시간을 서비스로 주입하는 것보다는 값으로 주입하는 것이 더 낫다.
    • 일반 값으로 작업하는 것이 더 쉽고, 테스트에서 해당 값을 스텁으로 처리하기도 더 쉽다.
interface DateTimeServer {  
    fun now(): LocalDateTime  
}  
  
class DefaultDateTimeServer: DateTimeServer {  
    override fun now(): LocalDateTime {  
       return LocalDateTime.now()  
    }  
}  
  
class InquiryController(  
    private val dateTimeServer: DateTimeServer // 시간을 서비스로 주입  
) {  
  
    fun approveInquiry(id: Int) {  
       val inquiry: Inquiry = getById(id)  
  
       inquiry.approve(dateTimeServer.now()) // 시간을 일반 값으로 주입  
       saveInquiry(inquiry)  
    }  
}