JUnit 및 Mockito를 사용하여 Java에서 테스트
I 자동화된 테스트 이는 코드의 품질과 유지 관리 가능성을 보장하는 데 필수적입니다. JUnit은 Java 단위 테스트를 위한 표준 프레임워크인 반면, Mockito를 사용하면 모의 개체를 만들 수 있습니다.
무엇을 배울 것인가
- JUnit 5: 주석 및 어설션
- 매개변수화된 테스트
- Mockito: 모의, 스텁 및 검증
- 테스트 주도 개발(TDD)
- 테스트 모범 사례
JUnit 5 - 기초
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class CalcolatriceTest {
private Calcolatrice calc;
@BeforeEach
void setUp() {
calc = new Calcolatrice();
}
@Test
@DisplayName("Addizione di due numeri positivi")
void testAddizione() {
assertEquals(5, calc.somma(2, 3));
}
@Test
void testSottrazione() {
assertEquals(2, calc.sottrai(5, 3));
}
@Test
void testDivisionePerZero() {
assertThrows(ArithmeticException.class, () -> {
calc.dividi(10, 0);
});
}
@Test
@Disabled("Test disabilitato temporaneamente")
void testDaImplementare() {
fail("Non ancora implementato");
}
}
class Calcolatrice {
int somma(int a, int b) { return a + b; }
int sottrai(int a, int b) { return a - b; }
int dividi(int a, int b) { return a / b; }
}
JUnit 5 주석
| 주석 | 설명 |
|---|---|
| @시험 | 메서드를 테스트로 표시 |
| @BeforeEach | 각 테스트 전에 수행됨 |
| @AfterEach | 각 테스트 후에 수행됨 |
| @비포올 | 모든 테스트 전에 한 번 실행 |
| @DisplayName | 읽을 수 있는 테스트 이름 |
| @장애가 있는 | 테스트를 일시적으로 비활성화합니다. |
고급 어설션
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
import java.time.Duration;
import java.util.*;
class AsserzioniTest {
@Test
void asserzioniBase() {
// Uguaglianza
assertEquals(4, 2 + 2);
assertNotEquals(5, 2 + 2);
// Booleani
assertTrue(3 > 2);
assertFalse(2 > 3);
// Null
Object obj = null;
assertNull(obj);
assertNotNull(new Object());
// Same (stessa istanza)
String s1 = "hello";
String s2 = s1;
assertSame(s1, s2);
}
@Test
void asserzioniCollezioni() {
List<String> lista = Arrays.asList("A", "B", "C");
assertEquals(3, lista.size());
assertTrue(lista.contains("B"));
// Array
int[] expected = {1, 2, 3};
int[] actual = {1, 2, 3};
assertArrayEquals(expected, actual);
}
@Test
void asserzioniEccezioni() {
Exception ex = assertThrows(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException("Messaggio errore");
});
assertEquals("Messaggio errore", ex.getMessage());
}
@Test
void asserzioniTimeout() {
assertTimeout(Duration.ofSeconds(2), () -> {
Thread.sleep(500); // Deve completare entro 2 secondi
});
}
@Test
void asserzioniRaggruppate() {
Studente s = new Studente("Mario", 25);
assertAll("Verifica studente",
() -> assertEquals("Mario", s.getNome()),
() -> assertEquals(25, s.getEta()),
() -> assertTrue(s.getEta() > 0)
);
}
}
class Studente {
private String nome;
private int eta;
Studente(String nome, int eta) { this.nome = nome; this.eta = eta; }
String getNome() { return nome; }
int getEta() { return eta; }
}
매개변수화된 테스트
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
import static org.junit.jupiter.api.Assertions.*;
class TestParametrizzatiDemo {
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5})
void testNumeriPositivi(int numero) {
assertTrue(numero > 0);
}
@ParameterizedTest
@ValueSource(strings = {"hello", "world", "java"})
void testStringheNonVuote(String str) {
assertFalse(str.isEmpty());
}
@ParameterizedTest
@CsvSource({
"1, 1, 2",
"2, 3, 5",
"10, 20, 30"
})
void testSomma(int a, int b, int risultato) {
assertEquals(risultato, a + b);
}
@ParameterizedTest
@MethodSource("fornisciNumeri")
void testConMethodSource(int numero, boolean atteso) {
assertEquals(atteso, numero % 2 == 0);
}
static Stream<Arguments> fornisciNumeri() {
return Stream.of(
Arguments.of(2, true),
Arguments.of(3, false),
Arguments.of(4, true)
);
}
@ParameterizedTest
@EnumSource(Mese.class)
void testMesi(Mese mese) {
assertNotNull(mese);
}
enum Mese { GENNAIO, FEBBRAIO, MARZO }
}
Mockito - 모의 객체
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
// Interfaccia da mockare
interface UserRepository {
User findById(Long id);
void save(User user);
List<User> findAll();
}
class User {
private Long id;
private String nome;
User(Long id, String nome) {
this.id = id;
this.nome = nome;
}
Long getId() { return id; }
String getNome() { return nome; }
}
class UserService {
private final UserRepository repository;
UserService(UserRepository repository) {
this.repository = repository;
}
User getUser(Long id) {
return repository.findById(id);
}
void createUser(User user) {
repository.save(user);
}
}
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository mockRepository;
@InjectMocks
private UserService userService;
@Test
void testGetUser() {
// Arrange: configura il mock
User userAtteso = new User(1L, "Mario");
when(mockRepository.findById(1L)).thenReturn(userAtteso);
// Act: esegui il metodo
User risultato = userService.getUser(1L);
// Assert: verifica
assertEquals("Mario", risultato.getNome());
verify(mockRepository).findById(1L);
}
@Test
void testCreateUser() {
User nuovoUser = new User(2L, "Luigi");
userService.createUser(nuovoUser);
// Verifica che save sia stato chiamato
verify(mockRepository, times(1)).save(nuovoUser);
}
@Test
void testUserNonTrovato() {
when(mockRepository.findById(99L)).thenReturn(null);
User risultato = userService.getUser(99L);
assertNull(risultato);
}
}
고급 모키토
import org.mockito.*;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.*;
class MockitoAvanzatoTest {
@Test
void testStubAvanzato() {
List<String> mockList = mock(List.class);
// Stub con any matcher
when(mockList.get(anyInt())).thenReturn("elemento");
assertEquals("elemento", mockList.get(5));
// Stub con comportamento diverso
when(mockList.size())
.thenReturn(1)
.thenReturn(2)
.thenReturn(3);
assertEquals(1, mockList.size()); // Prima chiamata
assertEquals(2, mockList.size()); // Seconda
assertEquals(3, mockList.size()); // Terza e successive
// Stub che lancia eccezione
when(mockList.get(100)).thenThrow(new IndexOutOfBoundsException());
}
@Test
void testVerifyAvanzato() {
List<String> mockList = mock(List.class);
mockList.add("uno");
mockList.add("due");
mockList.add("uno");
// Verifica numero chiamate
verify(mockList, times(2)).add("uno");
verify(mockList, times(1)).add("due");
verify(mockList, never()).add("tre");
verify(mockList, atLeast(1)).add(anyString());
verify(mockList, atMost(3)).add(anyString());
}
@Test
void testArgumentCaptor() {
UserRepository mockRepo = mock(UserRepository.class);
UserService service = new UserService(mockRepo);
// Cattura l'argomento passato a save
ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
service.createUser(new User(1L, "Test"));
verify(mockRepo).save(captor.capture());
User userCatturato = captor.getValue();
assertEquals("Test", userCatturato.getNome());
}
@Test
void testSpyParziale() {
// Spy: oggetto reale con alcune parti mockate
List<String> realList = new ArrayList<>();
List<String> spyList = spy(realList);
spyList.add("elemento"); // Chiama metodo reale
assertEquals(1, spyList.size());
// Override di un metodo specifico
doReturn(100).when(spyList).size();
assertEquals(100, spyList.size());
}
}
테스트 주도 개발(TDD)
// TDD segue il ciclo: RED -> GREEN -> REFACTOR
// STEP 1 - RED: Scrivi il test PRIMA del codice
class CarrelloTest {
@Test
void carrelloVuotoHaTotaleZero() {
Carrello carrello = new Carrello();
assertEquals(0.0, carrello.getTotale(), 0.001);
}
@Test
void aggiungiProdottoCalcolaTotale() {
Carrello carrello = new Carrello();
Prodotto p = new Prodotto("Libro", 29.99);
carrello.aggiungi(p);
assertEquals(29.99, carrello.getTotale(), 0.001);
}
@Test
void applicaSconto10Percento() {
Carrello carrello = new Carrello();
carrello.aggiungi(new Prodotto("A", 100.0));
carrello.applicaSconto(10);
assertEquals(90.0, carrello.getTotale(), 0.001);
}
}
// STEP 2 - GREEN: Scrivi il codice minimo per far passare il test
class Prodotto {
String nome;
double prezzo;
Prodotto(String nome, double prezzo) {
this.nome = nome;
this.prezzo = prezzo;
}
}
class Carrello {
private List<Prodotto> prodotti = new ArrayList<>();
private double scontoPercentuale = 0;
void aggiungi(Prodotto p) {
prodotti.add(p);
}
double getTotale() {
double totale = prodotti.stream()
.mapToDouble(p -> p.prezzo)
.sum();
return totale * (1 - scontoPercentuale / 100);
}
void applicaSconto(double percentuale) {
this.scontoPercentuale = percentuale;
}
}
// STEP 3 - REFACTOR: Migliora il codice mantenendo i test verdi
모범 사례
효과적인 테스트를 위한 규칙
- 테스트를 위한 논리적 설명: 집중 테스트
- 설명적인 이름: 테스트 내용을 설명합니다.
- AAA 패턴: 편곡, 행동, 주장
- 독립적인 테스트: 순서에 의존하지 마십시오
- 빠른 테스트: 자주 해야 한다
- 외부 종속성만 모의: 데이터베이스, API, 파일
- 깨지기 쉬운 테스트를 피하세요: 구현을 테스트하지 않음
- 커버력이 전부는 아니다: 품질 > 수량







