Siete alla ricerca di un modo per realizzare i test in modo più semplice? Abbiamo trovato il modo! Consultate il seguente articolo e scoprite come renderlo possibile.
Lo sviluppo di applicazioni moderne si basa su una semplice regola:
Utilizzare la composizione
Componiamo classi, funzioni e servizi in pezzi di software più grandi. Quest'ultimo elemento è il fondamento dei microservizi e dei servizi. architettura esagonale. Vorremmo utilizzare le soluzioni esistenti, integrarle con il nostro software e passare direttamente alla mercato.
Volete gestire la registrazione dell'account e memorizzare i dati dell'utente? Potete scegliere uno dei servizi OAuth. Forse la vostra applicazione offre un qualche tipo di abbonamento o di pagamento? Ci sono molti servizi che possono aiutarvi a gestire questo aspetto. Avete bisogno di analisi sul vostro sito web, ma non conoscete il GDPR? Non esitate a scegliere una delle soluzioni pronte all'uso.
Qualcosa che rende lo sviluppo così facile da un punto di vista commerciale potrebbe farvi venire il mal di testa nel momento in cui dovete scrivere un semplice test.
Le bestie fantastiche: Code, database e come testarli
I test unitari sono piuttosto semplici. Se ci si attiene solo alle regole, l'ambiente di test e il codice sono sane. Quali sono le regole?
Facile da scrivere - un test unitario dovrebbe essere facile da scrivere, perché se ne scrivono molti. Meno sforzo significa scrivere più test.
Leggibile - il codice di test deve essere facile da leggere. Il test è una storia. Descrive il comportamento del software e può essere usato come scorciatoia per la documentazione. Un buon test unitario aiuta a risolvere i bug senza dover eseguire il debug del codice.
Affidabile - il test dovrebbe fallire solo se c'è un bug nel sistema che si sta testando. È ovvio? Non sempre. A volte i test passano se li si esegue uno per uno, ma falliscono quando li si esegue come un insieme. Passano sulla vostra macchina, ma falliscono sul CI (Funziona sul mio computer). Un buon test unitario ha un solo motivo di fallimento.
Veloce - i test devono essere veloci. La preparazione all'esecuzione, l'avvio e l'esecuzione stessa dei test devono essere molto rapidi. Altrimenti li scriverete, ma non li eseguirete. Test lenti significano perdita di concentrazione. Si aspetta e si guarda la barra di avanzamento.
Indipendente - infine, il test deve essere indipendente. Questa regola deriva dalle precedenti. Solo i test veramente indipendenti possono diventare un'unità. Non interferiscono tra loro, possono essere eseguiti in qualsiasi ordine e i potenziali fallimenti non dipendono dai risultati di altri test. Indipendente significa anche nessuna dipendenza da risorse esterne come database, servizi di messaggistica o file system. Se è necessario comunicare con gli esterni, si possono usare mock, stub o dummies.
Tutto si complica quando vogliamo scrivere dei test di integrazione. Non è male se vogliamo testare alcuni servizi insieme. Ma quando dobbiamo testare servizi che utilizzano risorse esterne, come database o servizi di messaggistica, allora ci troviamo in difficoltà.
Per eseguire il test, è necessario installare...
Molti anni fa, quando volevamo realizzare alcuni test di integrazione e utilizzare, ad esempio, i database, avevamo due opzioni:
Possiamo installare un database in locale. Impostare uno schema e connettersi dal nostro test;
Possiamo collegarci a un'istanza esistente "da qualche parte nello spazio".
Entrambi hanno dei pro, entrambi hanno dei contro. Ma entrambi introducono ulteriori livelli di complessità. A volte si trattava di complessità tecnica derivante dalle caratteristiche di alcuni strumenti, ad esempio l'installazione e la gestione di un DB Oracle su localhost. A volte si trattava di un inconveniente nel processo, ad esempio la necessità di accordarsi con il test squadra sull'uso di JMS... ogni volta che si vogliono eseguire i test.
Contenitori in soccorso
Negli ultimi 10 anni, l'idea della containerizzazione si è affermata nel settore. Quindi, la decisione naturale è quella di scegliere i container come soluzione per il nostro problema di test di integrazione. Si tratta di una soluzione semplice e pulita. Basta eseguire la build del processo e tutto funziona! Non ci credete? Date un'occhiata a questa semplice configurazione di una build di maven:
com.dkanejs.maven.plugins
docker-compose-maven-plugin
4.0.0
up
test-compile
up
${progetto.basedir}/docker-compose.yml
true
down
post-integrazione-test
down
${project.basedir}/docker-compose.yml
true
E il docker-compose.yml Anche il file è molto bello!
L'esempio precedente è molto semplice. Solo un database postgres, pgAdmin e tutto il resto. Quando si esegue
bash
$ mvn clean verify
poi il plugin di maven avvia i contenitori e dopo i test li spegne. I problemi iniziano quando il progetto cresce e anche il nostro file di composizione cresce. Ogni volta è necessario avviare tutti i contenitori, che rimarranno in vita per l'intera compilazione. Si può migliorare un po' la situazione modificando la configurazione di esecuzione dei plugin, ma non è sufficiente. Nel peggiore dei casi, i contenitori esauriscono le risorse del sistema prima dell'avvio dei test!
E questo non è l'unico problema. Non è possibile eseguire un singolo test di integrazione dall'IDE. Prima di ciò, è necessario avviare i contenitori a mano. Inoltre, l'esecuzione successiva di maven distruggerà i contenitori (si veda la sezione giù esecuzione).
Quindi questa soluzione è come una grande nave da carico. Se tutto funziona bene, allora è tutto ok. Qualsiasi comportamento inaspettato o insolito ci porta a un qualche tipo di disastro.
Contenitori di test: eseguire i contenitori dai test
Ma cosa succederebbe se potessimo eseguire i nostri container dai test? L'idea sembra buona e viene già implementata. Contenitori di provaPoiché stiamo parlando di questo progetto, ecco una soluzione per i nostri problemi. Non è l'ideale, ma nessuno è perfetto.
Questo è un Java che supporta i test JUnit e Spock, fornendo modi leggeri e semplici per eseguire il contenitore Docker. Diamogli un'occhiata e scriviamo un po' di codice!
Prerequisiti e configurazione
Prima di iniziare, dobbiamo verificare la nostra configurazione. Contenitori di prova necessità:
Docker nella versione v17.09,
Java versione minima 1.8,
Accesso alla rete, in particolare a docker.hub.
Per maggiori informazioni sui requisiti per i sistemi operativi e gli IC specifici, consultare la sezione in documentazione.
Ora è il momento di aggiungere alcune righe a pom.xml.
org.testcontainers
testcontainers-bom
${testcontaines.version}
pom
import
org.postgresql
postgresql
runtime
org.testcontainers
postgresql
test
org.testcontainers
junit-jupiter
test
Uso Contenitori di prova versione 1.17.3, ma sentitevi liberi di usare quello più recente.
Test con il contenitore Postgres
Il primo passo è preparare l'istanza di un contenitore. È possibile farlo direttamente nel test, ma è meglio utilizzare una classe indipendente.
public class Postgres13TC extends PostgreSQLContainer {
private static final Postgres13TC TC = new Postgres13TC();
private Postgres13TC() {
super("postgres:13.2");
}
public static Postgres13TC getInstance() {
return TC;
}
@Override
public void start() {
super.start();
System.setProperty("DB_URL", TC.getJdbcUrl());
System.setProperty("DB_USERNAME", TC.getUsername());
System.setProperty("DB_PASSWORD", TC.getPassword());
}
@Override
public void stop() {
// non fa nulla. Questa è un'istanza condivisa. Lasciamo che sia la JVM a gestire questa operazione.
}
}
All'inizio dei test, creeremo un'istanza di Postgres13TC. Questa classe può gestire le informazioni sul nostro contenitore. Le più importanti sono le stringhe di connessione al database e le credenziali. Ora è il momento di scrivere un test molto semplice.
@Contenitori di test
classe SimpleDbTest {
@Container
private static Postgres13TC = Postgres13TC.getInstance();
@Test
void testConnection() {
assumeThat(postgres13TC.isRunning());
var connectionProps = new Properties();
connectionProps.put("user", postgres13TC.getUsername());
connectionProps.put("password", postgres13TC.getPassword());
try (Connection = DriverManager.getConnection(postgres13TC.getJdbcUrl(),
connectionProps)) {
var resultSet = connection.prepareStatement("Select 1").executeQuery();
resultSet.next();
assertThat(resultSet.getInt(1)).isEqualTo(1);
} catch (SQLException sqlException) {
assertThat((Exception) sqlException).doesNotThrowAnyException();
}
}
}
Qui uso JUnit 5. Annotazione @Testcontainers fa parte delle estensioni che controllano i contenitori nell'ambiente di test. Trovano tutti i campi con @Contenitore e i contenitori start e stop, rispettivamente.
Test con Spring Boot
Come ho già detto, nel progetto utilizzo Spring Boot. In questo caso, dobbiamo scrivere un po' di codice in più. Il primo passo è creare una classe di configurazione aggiuntiva.
Questa classe sovrascrive le proprietà esistenti con i valori della classe contenitore di prova. Le prime tre proprietà sono proprietà standard di Spring. Le cinque successive sono proprietà aggiuntive e personalizzate, che possono essere utilizzate per configurare altre risorse ed estensioni come liquibase, ad esempio:
Ora è il momento di definire un semplice test di integrazione.
@SpringBootTest(webEnvironment = RANDOM_PORT)
@AutoConfigureTestDatabase(replace = NONE)
@ContextConfiguration(initializers = ContainerInit.class)
@Contenitori di test
class DummyRepositoryTest {
@Autowired
private DummyRepository;
@Test
void shouldReturnDummy() {
var byId = dummyRepository.getById(10L);
var expected = new Dummy();
expected.setId(10L);
assertThat(byId).complet().emitsCount(1).emits(expected);
}
}
Qui abbiamo alcune annotazioni aggiuntive.
@SpringBootTest(webEnvironment = RANDOM_PORT) - contrassegna il test come test Spring Boot e avvia il contesto Spring.
@AutoConfigureTestDatabase(replace = NONE) - queste annotazioni dicono che l'estensione Spring Test non deve sostituire la configurazione del database postgres con H2 nella configurazione della memoria.
@ContextConfiguration(initializers = ContainerInit.class) - un ulteriore contesto di primavera in cui si impostano le proprietà da Contenitori di prova.
@Testcontainers - come già detto, questa annotazione controlla il ciclo di vita del contenitore.
In questo esempio, utilizzo repository reattivi, ma funziona allo stesso modo con i comuni repository JDBC e JPA.
Ora possiamo eseguire il test. Se è la prima esecuzione, il motore deve prelevare le immagini da docker.hub. Potrebbe volerci un attimo. Dopodiché, vedremo che sono stati eseguiti due contenitori. Uno è postgres e l'altro è il controller Testcontainers. Il secondo contenitore gestisce i contenitori in esecuzione e, anche se la JVM si arresta inaspettatamente, spegne i contenitori e pulisce l'ambiente.
Riassumiamo
Contenitori di prova sono strumenti molto semplici da usare che ci aiutano a creare test di integrazione che utilizzano i container Docker. Questo ci offre maggiore flessibilità e aumenta la velocità di sviluppo. L'impostazione corretta della configurazione dei test riduce il tempo necessario per i nuovi sviluppatori. Non devono impostare tutte le dipendenze, ma solo eseguire i test scritti con i file di configurazione selezionati.