LocalDateTime.now() 를 직접 쓴다밤 10시가 넘으면 영업시간 검증 테스트가 갑자기 깨집니다. 원인은 실제 현재 시각을 쓴 것이고, 해결은 Clock 을 주입받아 Clock.fixed 로 고정하는 것입니다. CI 서버의 시간대가 달라도 결과가 항상 같아집니다.
static 필드에 상태를 쌓거나, 앞 테스트가 커밋한 행을 다음 테스트가 전제로 삼으면 실행 순서를 바꾸는 순간 깨집니다. @BeforeEach 로 매번 새로 만들고, DB 는 트랜잭션 롤백으로 격리해야 합니다. JUnit 은 기본적으로 순서를 보장하지 않습니다.
@Test 안에서 값을 출력만 하고 assert 가 하나도 없으면, 예외만 안 터지면 항상 초록불입니다. 로직이 완전히 잘못돼도 잡히지 않습니다. 확인하려는 값마다 최소 하나의 assertEquals/assertTrue 가 있어야 합니다.
정상·실패·경계·예외를 한 메서드에 다 넣으면 하나가 깨졌을 때 나머지 검증까지 실행이 멈춰 원인이 뭔지 알기 어렵습니다. 시나리오마다 메서드를 나누거나, 경계값은 @ParameterizedTest 로 뽑아냅니다.
verify(repo).save(any()), verify(repo, times(3)).findById(any()) 를 줄줄이 나열하면 테스트가 구현 순서를 그대로 따라간 것뿐입니다. 구현을 리팩터링만 해도 테스트가 깨집니다. 결과 상태를 검증하는 편이 대부분 더 안전합니다.
테스트가 실행될 때마다 운영 데이터가 바뀌거나, 동시에 돌리면 서로 데이터를 덮어씁니다. H2 메모리 DB 로 스키마를 똑같이 맞추고, 트랜잭션 롤백으로 격리해야 안전합니다.
BigDecimal.equals 로 비교한다assertEquals(new BigDecimal("1000"), new BigDecimal("1000.00")); // 실패! 스케일이 다름equals 는 값이 같아도 스케일이 다르면 false 를 돌려줍니다. assertEquals(0, a.compareTo(b)) 로 비교하거나 assertThat(a).isEqualByComparingTo(b) 를 씁니다.
try { service.place("kim", BigDecimal.ZERO); fail("예외가 나야 함"); }
catch (IllegalArgumentException e) { /* ok */ }assertThrows 한 줄이면 되는데 4줄로 늘어나고, 다른 예외가 나도 통과해 버릴 수 있습니다. assertThrows(IllegalArgumentException.class, () -> ...) 로 바꿉니다.
리플렉션으로 private 메서드를 억지로 호출하는 코드는 구현이 바뀌면 바로 깨집니다. discountFor 처럼 순수 로직은 static 이나 별도 클래스로 빼서 public 으로 두면, 억지 없이 테스트할 수 있습니다.
@SpringBootTest 를 모든 테스트에 붙인다컨텍스트 전체를 매번 새로 띄우면 테스트 하나에 수 초가 걸립니다. 순수 로직은 OrderServiceTest 처럼 컨텍스트 없이, DB 접근만 @MybatisTest/@JdbcTest 로, 전체 배선 확인은 소수의 @SpringBootTest 로 나눕니다.