- 안티 패턴: 겉으로 적절한 것처럼 보이지만 장래에 더 큰 문제로 이어지는 반복적인 문제
비공개 메서드 단위 테스트#
비공개 메서드와 테스트 취약성#
- 비공개 메서드를 노출하는 경우 식별할 수 있는 동작만 테스트하는 것을 위반한다.
- 비공개 메서드를 노출하면 테스트가 구현 세부 사항과 결합되고 결과적으로 리팩터링 내성이 떨어진다.
비공개 메서드와 불필요한 커버리지#
- 비공개 메서드가 너무 복잡해서 식별할 수 있는 동작으로 테스트하기에 충분히 커버리지를 얻을 수 없는 경우가 있다. 이런 경우 두 가지 문제가 존재할 수 있다.
- 죽은 코드다. 테스트에서 벗어난 코드가 어디에도 사용되지 않는다면 리팩터링 후에도 남아서 관계없는 코드일 수 있다. 이러한 코드는 삭제하는 것이 좋다.
- 추상화가 누락돼 있다. 비공개 메서드가 너무 복잡하면 별도의 클래스로 도출해야 하는 추상화가 누락됐다는 징후다.
비공개 메서드 테스트가 타당한 경우#
- 비공개 메서드를 테스트하는 것 자체는 나쁘지 않다. 비공개 메서드가 구현 세부 사항의 프록시에 해당하므로 나쁜 것이다.
- 비공개이면서 식별할 수 있는 동작(예: 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)
}
}