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 | 各テスト後に実行される |
| @BeforeAll | すべてのテストの前に 1 回実行する |
| @表示名 | テストの読みやすい名前 |
| @無効 | テストを一時的に無効にする |
高度なアサーション
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、ファイル
- 脆弱なテストを避ける: 実装をテストしません
- 報道だけがすべてではない: 質 > 量







