JavaRush /Java Blog /Random-IT /Java Unit Testing: tecniche, concetti, pratica

Java Unit Testing: tecniche, concetti, pratica

Pubblicato nel gruppo Random-IT
Oggi difficilmente troverai un'applicazione che non sia coperta da test, quindi questo argomento sarà più attuale che mai per gli sviluppatori alle prime armi: senza test non puoi arrivare da nessuna parte. Come pubblicità, ti suggerisco di guardare i miei articoli passati. Alcuni di essi riguardano test (e anche così gli articoli saranno molto utili):
  1. Test di integrazione di un database utilizzando MariaDB per sostituire MySql
  2. Implementazione dell'applicazione multilingue
  3. Salvataggio di file nell'applicazione e dati su di essi nel database
Consideriamo quali tipi di test vengono utilizzati in linea di principio, quindi studieremo in dettaglio tutto ciò che devi sapere sui test unitari.

Tipi di test

Cos'è un test? Come dice Wiki: “ Un test o una prova è un modo per studiare i processi sottostanti di un sistema posizionando il sistema in diverse situazioni e monitorando i cambiamenti osservabili in esso”. In altre parole, questo è un test del corretto funzionamento del nostro sistema in determinate situazioni. Tutto su Unit test: metodi, concetti, pratica - 2Bene, vediamo quali tipologie di test esistono:
  1. I test unitari sono test il cui compito è testare individualmente ciascun modulo del sistema. È auspicabile che si tratti di parti minimamente divisibili del sistema, ad esempio moduli.

  2. Il test di sistema è un test di alto livello per testare il funzionamento di una parte più ampia di un'applicazione o del sistema nel suo complesso.

  3. Il test di regressione è un test utilizzato per verificare se nuove funzionalità o correzioni di bug influiscono sulla funzionalità esistente dell'applicazione e se ricompaiono vecchi bug.

  4. Il test funzionale verifica la conformità di parte dell'applicazione con i requisiti indicati nelle specifiche, nelle storie degli utenti, ecc.

    Tipi di test funzionali:

    • test “white box” per la conformità di parte dell'applicazione ai requisiti con conoscenza dell'implementazione interna del sistema;
    • Test “scatola nera” per la conformità di parte dell'applicazione ai requisiti senza conoscenza dell'implementazione interna del sistema.
  5. Il test delle prestazioni è un tipo di test scritto per determinare la velocità con cui un sistema o parte di esso funziona sotto un determinato carico.
  6. Test di carico - test progettati per verificare la stabilità del sistema sotto carichi standard e per trovare il picco massimo possibile al quale l'applicazione funziona correttamente.
  7. Lo stress test è un tipo di test progettato per verificare la funzionalità di un'applicazione sotto carichi non standard e per determinare il picco massimo possibile al quale il sistema non si bloccherà.
  8. Test di sicurezza : test utilizzati per verificare la sicurezza di un sistema (da attacchi di hacker, virus, accesso non autorizzato a dati riservati e altri piaceri della vita).
  9. Il test di localizzazione è il test di localizzazione per un'applicazione.
  10. Il test di usabilità è un tipo di test volto a verificare l'usabilità, la comprensibilità, l'attrattiva e l'apprendibilità per gli utenti.
  11. Tutto ciò sembra positivo, ma come funziona nella pratica? È semplice: viene utilizzata la piramide dei test di Mike Cohn: Tutto su Unit test: metodi, concetti, pratica - 4questa è una versione semplificata della piramide: ora è divisa in parti più piccole. Ma oggi non pervertiremo e considereremo l'opzione più semplice.
    1. Unit : test unitari utilizzati in vari livelli dell'applicazione, testando la logica divisibile più piccola dell'applicazione: ad esempio, una classe, ma molto spesso un metodo. Questi test solitamente cercano di isolarsi il più possibile dalla logica esterna, cioè di creare l'illusione che il resto dell'applicazione funzioni in modalità standard.

      Dovrebbero esserci sempre molti di questi test (più di altri tipi), poiché testano piccoli pezzi e sono molto leggeri, non consumano molte risorse (per risorse intendo RAM e tempo).

    2. Integrazione : test di integrazione. Controlla parti più grandi del sistema, ovvero la combinazione di più parti logiche (diversi metodi o classi) o la correttezza di lavorare con un componente esterno. Di solito ci sono meno test di questo tipo rispetto ai test unitari, poiché sono più pesanti.

      Come esempio di test di integrazione, puoi considerare la connessione a un database e la verifica che i metodi che lo utilizzano funzionino correttamente .

    3. UI : test che controllano il funzionamento dell'interfaccia utente. Influenzano la logica a tutti i livelli dell'applicazione, motivo per cui sono anche chiamati end-to-end. Di norma ce ne sono molti meno, poiché sono i più pesanti e devono controllare i percorsi più necessari (utilizzati).

      Nella figura sopra vediamo il rapporto tra le aree delle diverse parti del triangolo: approssimativamente la stessa proporzione viene mantenuta nel numero di queste prove nel lavoro reale.

      Oggi daremo uno sguardo più da vicino ai test più utilizzati: gli unit test, poiché tutti gli sviluppatori Java che si rispettino dovrebbero essere in grado di utilizzarli a livello base.

    Concetti chiave del test unitario

    La copertura del test (Code Coverage) è una delle principali valutazioni della qualità del test dell'applicazione. Questa è la percentuale di codice coperta dai test (0-100%). In pratica, molte persone inseguono questa percentuale, cosa che non condivido, poiché iniziano ad aggiungere test dove non sono necessari. Ad esempio, il nostro servizio prevede operazioni CRUD standard (crea/ottieni/aggiorna/elimina) senza logica aggiuntiva. Questi metodi sono puramente intermediari che delegano il lavoro al livello che funziona con il repository. In questa situazione non abbiamo nulla da verificare: forse se questo metodo richiama un metodo del Tao, ma questo non è grave. Per valutare la copertura del test vengono solitamente utilizzati strumenti aggiuntivi: JaCoCo, Cobertura, Clover, Emma, ​​ecc. Per uno studio più approfondito di questo problema, conserva un paio di articoli adatti: TDD (sviluppo basato sui test) - sviluppo basato sui test. In questo approccio, prima di tutto, viene scritto un test che controllerà un codice specifico. Risulta essere un test della scatola nera: sappiamo cosa c'è in input e sappiamo cosa dovrebbe accadere in output. Ciò evita la duplicazione del codice. Lo sviluppo basato sui test inizia con la progettazione e lo sviluppo di test per ogni piccola funzionalità dell'applicazione. Nell'approccio TDD, innanzitutto, viene sviluppato un test che definisce e verifica cosa farà il codice. L'obiettivo principale di TDD è rendere il codice più chiaro, più semplice e privo di errori. Tutto su Unit test: metodi, concetti, pratica - 6L’approccio è costituito dai seguenti componenti:
    1. Stiamo scrivendo il nostro test.
    2. Eseguiamo il test, che sia passato o meno (vediamo che è tutto rosso, non spaventarti: dovrebbe essere così).
    3. Aggiungiamo il codice che dovrebbe soddisfare questo test (esegui il test).
    4. Rifattorizziamo il codice.
    Basandosi sul fatto che i test unitari sono gli elementi più piccoli nella piramide dell'automazione dei test, TDD si basa su di essi. Con l'aiuto dei test unitari possiamo testare la logica di business di qualsiasi classe. BDD (sviluppo guidato dal comportamento) - sviluppo attraverso il comportamento. Questo approccio si basa sul TDD. Più specificamente, utilizza esempi scritti in un linguaggio chiaro (solitamente in inglese) che illustrano il comportamento del sistema a tutti coloro che sono coinvolti nello sviluppo. Non approfondiremo questo termine, poiché colpisce principalmente tester e analisti aziendali. Test Case - uno script che descrive i passaggi, le condizioni specifiche e i parametri necessari per verificare l'implementazione del codice in prova. Fixture è uno stato dell'ambiente di test necessario per la corretta esecuzione del metodo in prova. Si tratta di un insieme predeterminato di oggetti e del loro comportamento nelle condizioni utilizzate.

    Fasi di test

    Il test si compone di tre fasi:
    1. Specificare i dati da testare (infissi).
    2. Utilizzando il codice sotto test (chiamando il metodo sotto test).
    3. Controllare i risultati e confrontarli con quelli attesi.
    Tutto su Unit test: metodi, concetti, pratica - 7Per garantire la modularità dei test, è necessario essere isolati dagli altri livelli dell'applicazione. Questo può essere fatto utilizzando stub, mock e spie. I mock sono oggetti personalizzabili (ad esempio, specifici per ciascun test) e consentono di impostare le aspettative per le chiamate di metodo sotto forma di risposte che prevediamo di ricevere. I controlli sulle aspettative vengono eseguiti tramite chiamate agli oggetti Mock. Stub : forniscono una risposta cablata alle chiamate durante i test. Possono anche memorizzare informazioni sulla chiamata (ad esempio parametri o il numero di queste chiamate). Questi sono talvolta chiamati con il loro termine: spia ( Spia ). A volte i termini stub e mock vengono confusi: la differenza è che uno stub non controlla nulla, ma simula solo un determinato stato. Un mock è un oggetto che ha aspettative. Ad esempio, un determinato metodo di classe deve essere chiamato un certo numero di volte. In altre parole, il tuo test non si interromperà mai a causa di uno stub, ma potrebbe interrompersi a causa di un mock.

    Ambienti di prova

    Quindi ora passiamo agli affari. Sono disponibili diversi ambienti di test (framework) per Java. I più popolari sono JUnit e TestNG. Per la nostra revisione utilizziamo: Tutto su Unit test: metodi, concetti, pratica - 8Un test JUnit è un metodo contenuto in una classe utilizzato solo per il test. Una classe ha in genere lo stesso nome della classe che sta testando con +Test alla fine. Ad esempio, CarService→ CarServiceTest. Il sistema di build Maven include automaticamente tali classi nell'area di test. In effetti, questa classe è chiamata classe di test. Esaminiamo un po' le annotazioni di base: @Test - definizione di questo metodo come metodo di test (in effetti, il metodo contrassegnato con questa annotazione è uno unit test). @Before : contrassegna il metodo che verrà eseguito prima di ogni test. Ad esempio, riempiendo i dati dei test della classe, leggendo i dati di input, ecc. @After - posizionato sopra il metodo che verrà chiamato dopo ogni test (pulizia dei dati, ripristino dei valori predefiniti). @BeforeClass - posizionato sopra il metodo - analogo a @Before. Ma questo metodo viene chiamato solo una volta prima di tutti i test per una determinata classe e quindi deve essere statico. Viene utilizzato per eseguire operazioni più gravose, come il sollevamento di un database di prova. @AfterClass è l'opposto di @BeforeClass: eseguito una volta per una determinata classe, ma eseguito dopo tutti i test. Utilizzato, ad esempio, per pulire le risorse persistenti o disconnettersi dal database. @Ignore : nota che il metodo seguente è disabilitato e verrà ignorato durante l'esecuzione dei test complessivi. Viene utilizzato in diversi casi, ad esempio, se il metodo di base è stato modificato e non c'era tempo per ripetere il test. In questi casi, è consigliabile aggiungere anche una descrizione - @Ignore("Alcune descrizioni"). @Test (expected = Exception.class) - utilizzato per test negativi. Si tratta di test che controllano come si comporta un metodo in caso di errore, ovvero il test si aspetta che il metodo lanci qualche eccezione. Tale metodo è indicato dall'annotazione @Test, ma con un errore da rilevare. @Test(timeout=100) - controlla che il metodo venga eseguito in non più di 100 millisecondi. @Mock - una classe viene utilizzata su un campo per impostare un dato oggetto come mock (questo non proviene dalla libreria Junit, ma da Mockito) e, se ne abbiamo bisogno, imposteremo il comportamento del mock in una situazione specifica , direttamente nel metodo di prova. @RunWith(MockitoJUnitRunner.class) - il metodo è posizionato sopra la classe. Questo è il pulsante per eseguire i test al suo interno. I runner possono essere diversi: ad esempio ci sono i seguenti: MockitoJUnitRunner, JUnitPlatform, SpringRunner, ecc.). In JUnit 5, l'annotazione @RunWith è stata sostituita dalla più potente annotazione @ExtendWith. Diamo un'occhiata ad alcuni metodi per confrontare i risultati:
    • assertEquals(Object expecteds, Object actuals)— controlla se gli oggetti trasmessi sono uguali.
    • assertTrue(boolean flag)— controlla se il valore passato restituisce true.
    • assertFalse(boolean flag)— controlla se il valore passato restituisce false.
    • assertNull(Object object)– controlla se l'oggetto è nullo.
    • assertSame(Object firstObject, Object secondObject)— controlla se i valori passati si riferiscono allo stesso oggetto.
    • assertThat(T t, Matcher<T> matcher)— controlla se t soddisfa la condizione specificata nel matcher.
    C'è anche un utile modulo di confronto da assertj - assertThat(firstObject).isEqualTo(secondObject) Qui ho parlato dei metodi di base, poiché il resto sono diverse varianti di quanto sopra.

    Pratica di prova

    Ora diamo un'occhiata al materiale di cui sopra utilizzando un esempio specifico. Testeremo il metodo per il servizio: aggiornamento. Non prenderemo in considerazione il livello dao, poiché è il nostro livello predefinito. Aggiungiamo uno starter per i test:
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-test</artifactId>
       <version>2.2.2.RELEASE</version>
       <scope>test</scope>
    </dependency>
    Quindi, la classe di servizio:
    @Service
    @RequiredArgsConstructor
    public class RobotServiceImpl implements RobotService {
       private final RobotDAO robotDAO;
    
       @Override
       public Robot update(Long id, Robot robot) {
           Robot found = robotDAO.findById(id);
           return robotDAO.update(Robot.builder()
                   .id(id)
                   .name(robot.getName() != null ? robot.getName() : found.getName())
                   .cpu(robot.getCpu() != null ? robot.getCpu() : found.getCpu())
                   .producer(robot.getProducer() != null ? robot.getProducer() : found.getProducer())
                   .build());
       }
    }
    8 - estrai l'oggetto aggiornato dal database 9-14 - crea l'oggetto tramite il builder, se l'oggetto in arrivo ha un campo - impostalo, in caso contrario - lascia ciò che è nel database E guarda il nostro test:
    @RunWith(MockitoJUnitRunner.class)
    public class RobotServiceImplTest {
       @Mock
       private RobotDAO robotDAO;
    
       private RobotServiceImpl robotService;
    
       private static Robot testRobot;
    
       @BeforeClass
       public static void prepareTestData() {
           testRobot = Robot
                   .builder()
                   .id(123L)
                   .name("testRobotMolly")
                   .cpu("Intel Core i7-9700K")
                   .producer("China")
                   .build();
       }
    
       @Before
       public void init() {
           robotService = new RobotServiceImpl(robotDAO);
       }
    1 — il nostro Runner 4 — isola il servizio dal livello dao sostituendolo con un mock 11 — imposta un'entità di test per la classe (quella che useremo come criceto di prova) 22 — imposta un oggetto di servizio che testeremo
    @Test
    public void updateTest() {
       when(robotDAO.findById(any(Long.class))).thenReturn(testRobot);
       when(robotDAO.update(any(Robot.class))).then(returnsFirstArg());
       Robot robotForUpdate = Robot
               .builder()
               .name("Vally")
               .cpu("AMD Ryzen 7 2700X")
               .build();
    
       Robot resultRobot = robotService.update(123L, robotForUpdate);
    
       assertNotNull(resultRobot);
       assertSame(resultRobot.getId(),testRobot.getId());
       assertThat(resultRobot.getName()).isEqualTo(robotForUpdate.getName());
       assertTrue(resultRobot.getCpu().equals(robotForUpdate.getCpu()));
       assertEquals(resultRobot.getProducer(),testRobot.getProducer());
    }
    Qui vediamo una chiara divisione del test in tre parti: 3-9 - impostazione dei dispositivi 11 - esecuzione della parte testata 13-17 - verifica dei risultati Maggiori dettagli: 3-4 - impostazione del comportamento per moka dao 5 - impostazione di un'istanza che aggiorneremo in aggiunta al nostro standard 11 - usa il metodo e prendi l'istanza risultante 13 - controlla che non sia zero 14 - controlla l'ID del risultato e gli argomenti del metodo specificati 15 - controlla se il nome è stato aggiornato 16 - guarda al risultato dalla CPU 17 - poiché non l'abbiamo impostato nel campo dell'istanza di aggiornamento, dovrebbe rimanere lo stesso, controlliamolo. Tutto su Unit test: metodi, concetti, pratica - 9Lanciamo: Tutto su Unit test: tecniche, concetti, pratica - 10il test è verde, puoi espirare)) Quindi, riassumiamo: il test migliora la qualità del codice e rende il processo di sviluppo più flessibile e affidabile. Immagina quanti sforzi dovremmo spendere per riprogettare il software con centinaia di file di classe. Una volta scritti i test unitari per tutte queste classi, possiamo effettuare il refactoring con sicurezza. E, cosa più importante, ci aiuta a trovare facilmente gli errori durante lo sviluppo. Ragazzi, per me oggi è tutto: mettete mi piace, scrivete commenti))) Tutto su Unit test: metodi, concetti, pratica - 11
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION