IT Industrija

🔥 Najčitanije
🔥 Najčitanije
Primeri u ovom tekstu su implementirani koristeći Java 8, Spring Boot, Spring Boot Test, JUnit, Testcontainers, Gradle i Liquibase. Pretpostavka je da je čitalac upoznat sa korišćenim tehonologijama, ali u svakom slučaju, osnovni principi i ideje iz ovog posta su primenljivi u slučaju da se testcontaineri koriste i u nekim drugim tehnologijama.
Testovi su neizostavni deo procesa razvoja softvera.
Ne možemo tvrditi da je funkcionalnost koju smo razvijali završena, ukoliko je prethodno nismo testirali, jer ćemo jedino testiranjem utvrditi da li funkcionalnost radi ono što specifikacije kažu da treba da treba da radi. Upotreba integracionih ili funkcionalnih testova je često zavisna od infrastrukture, poput relacione baze podataka, messiging queue-a, distrubuiranog keša itd.
Kada pravimo integracione testove, zgodno je da se poslužimo in-memory bazom. Kao i sve što je jednostavno za postavljanje i korišćenje, ovo nosi neke svoje nedostatke i izazove.
Tako recimo, čak iako testovi prolaze, ne možemo sa sigurnošću tvrditi da će testiran kod raditi u produkciji kako treba. Razlog tome može biti recimo da in-memory baza ne podržava neke funkcionalnosti koje su specifične za određenog proizvođača produkcione baze. Na primer, Postgres ima JSONB tip, koji nije podržan od strane H2 ili HSQLDB-a, isto tako primer je kolona virtual u Oracle i MySQL bazama. Ovo možemo prevazići tako što ćemo prilagoditi implementaciju i uskladiti je sa testinim kodom, ili jednostavno te scenarije nećemo pokriti testovima, što nije idealno, jer prilagođavamo implementaciju testovima, ili rizikujemo ostavljanje netestiranog koda.
Ovo su samo neki primeri vezani za relacione baze podataka, a u slučaju da imamo hibridni sistem sa NoSQL bazom, messaging queue-om, keširanjem… stvari se još više komplikuju.
Pročitaj i:
Jedna od opcija koju možemo da koristimo za prevazilaženje ovih problema je da testiramo u okruženju sličnom produkciji. Pošto je ovo okruženje verovatno već prisutno kao deo deployment i delivery procesa, ovo izgleda kao solidna opcija za potrebe testiranja. Ali i ovde imamo nedostataka. Glavni je taj što ne možemo da pokrećemo testove sa lokalnog razvojnog okruženja na jednostavan način (to podrazumeva dodatno konfigurisanje). Ovo lokalno okruženje nam možda nije uvek ni dostupno (možda se koristi za performance testove, ili je prosto nedostupno). Takođe, kada deploy-ujemo i pokrenemo našu aplikaciju i testove na takovom okruženju, feedback loop je sporiji, tako da se greške detektuju i popravljaju dosta kasnije.
Na sreću, postoji lakši način da prevaziđemo ove poteškoće upotrebom Testcontainera.
Primeri u ovom tekstu su implementirani koristeći Java 8, Spring Boot, Spring Boot Test, JUnit, Testcontainers, Gradle i Liquibase.
Pretpostavka je da je čitalac upoznat sa korišćenim tehonologijama, ali u svakom slučaju, osnovni principi i ideje iz ovog posta su primenljivi u slučaju da se testcontaineri koriste i u nekim drugom tehnologijama.
Postoje dva načina na koja možemo koristiti testcontainere — upotrebom Java test koda, ili upotrebom build alata poput Gradle-a. Pristup koji koristimo ovde će biti fokusiran na upotrebu Java koda. Drugi pristupi prevazilaze okvire ovog posta, te bih vas uputio da više detalja potražite u zvaničnoj dokumentaciji i primerima iz community-a.
Prvi korak je dodavanje dependency-ja u projekat. Za ovaj primer ćemo koristiti org.testcontainers.postgressql dependency, koji se specijalizuje za podršku Postgres docker containera. Postoji generički modul testcontainer dependency-ja, koji podržava generičke containere, docker-compose…za više informancija, konsultujte korisničku dokumentaciju. Dependency se dodaje build Gradle fajlu:
testImplementation(’org.testcontainers:postgresql:1.7.1’) // Napomena: u vreme pisanja ovog posta, ovo je najnovija verzija. Tokom rada treba da koristite najnoviju verziju.
Sada, kada imamo Testcontainer dependency na našem classpath-u, najlakši način da postavimo naš Postgres container je da kreiramo instancu na ovaj način:
final PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer();
Ovo će inicijalizovati Postgres container sa podrazumevanim podešavanjima (pogledati PostgreSQLContainer klasu za više detalja). Posle ovoga možemo da pokrenemo container pozivanjem:
postgreSQLContainer.start();
Mogli bismo takođe da napravimo instancu public i static i obeležimo je sa @ClassRule, što će automatski da pokrene i zaustavi container nakon što se završe testovi u klasi.
Container se inače pokreće na nasumičnom portu, da bi se izbegli potencijalni konflikti. Moguće je podesiti korisničko ime, lozinku i ime baze prilikom inicijalizacije:
private static final PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer().withUsername(“user01”).withPassword(“pass01”).withD atabaseName(“testDatabase”);
Međutim, nismo u mogućnosti da rekonfigurišemo port. Port se dodeljuje kada se container već pokrene, tako da ne postoji način kako da saznamo koji port će biti korišćen pre pokretanja containera.
Ovo može da predstavlja izazov, pod pretpostavkom da aplikacija ima datasource instancu konfigurisanu da komunicira sa databazom. U tom slučaju, moramo da konfigurišemo datasource instancu za naše testove.
Jedna od opcija ovde je da koristimo specijalizovanu container instancu
FixedHostPortGenericContainer: @ClassRule public static FixedHostPortGenericContainer postgreSQLContainer = new FixedHostPortGenericContainer<>("postgres:latest") .withEnv("POSTGRES_USER","testUser") .withEnv("POSTGRES_PASSWORD","testPassword") .withEnv("POSTGRES_DB","testDb") .withFixedExposedPort(60015);
U primeru iznad, popravili smo port 60015, pa sada pre nego što pokrenemo container možemo da izvršimo konfiguraciju naše datasource instance koristeći jdbc connection string:
"jdbc:postgresql://" + DockerClientFactory.instance().dockerHostIpAddress() + ":60015/testDb";
Ovaj pristup nije baš idealan, s obzirom na to da nemamo garanciju da će port 60015 uvek da ostane otvoren, i da nećemo imati neki konflikt. Uzimajući ovo u obzir, moramo da ostavimo dinamičku dodelu porta, ali da nekako iniciramo datasource instancu, i sa njom Liquidbase instancu koja može da bude korišćena za (re)kreiranje šeme baze podataka. Ovo će zahtevati podešavanje aplikacionog konteksta nakon pokretanja container-a. Tako na primer, možemo imati sledeću test konfiguracijsku klasu:
@TestConfiguration public class TestRdbsConfiguration { @Bean public PostgreSQLContainer postgreSQLContainer() { final PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer(); postgreSQLContainer.start(); return postgreSQLContainer; } @Bean public DataSource dataSource(final PostgreSQLContainer postgreSQLContainer) { // Datasource initialization ds.setJdbcUrl(postgreSQLContainer.getJdbcUrl()); ds.setUsername(postgreSQLContainer.getUsername()); ds.setPassword(postgreSQLContainer.getPassword()); ds.setDriverClassName(postgreSQLContainer.getDriverClassName()); // Additional parameters configuration omitted return ds; } @Bean public Liquibase liquibase(final DataSource dataSource) throws LiquibaseException, SQLException { final Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(dataSource.getConnection())); return new liquibase.Liquibase(Paths.get(".", this.get(".", PATH_TO_CHANGELOG_FILE).normalize().toAbsolutePath().toString(), new FileSystemResourceAccessor(), database); } }
Onda u našoj test klasi:
@RunWith(SpringRunner.class) @SpringBootTest(classes = TestRdbsConfiguration.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class SomeIntRdbsTest { @Autowire public PostgreSQLContainer postgreSQLContainer; @Autowired private Liquibase liquibase; // Recreate database scheme before each test so no data interdependencies are introduced @Before public void before() throws LiquibaseException { liquibase.dropAll(); liquibase.update(new Contexts()); } // Test methods ... }
Ovaj pristup je dosta bolji od predefinisane dodele porta container-u. Važno je samo ne zaboraviti postaviti test konfiguracijsku klasu u @SpringBootTest anotaciju. Na drugi, sličan pristup sam naišao istražujući temu testcontainera i spring boot testiranja. Ideja je da se napravi klasa za inicijalizaciju aplikacionog konteksta koja će stvoriti i konfigurisati Liquibase i datasource beanove nakon pokretanja containera. Dakle, na primer, definišemo klasu:
public class LbAndDsInitializer implements ApplicationContextInitializer<ConfigurableWebApplicationContext> { public static final ThreadLocal<PostgreSQLContainer> PG_CONTAINER = ThreadLocal.withInitial(() -> null); } we override initialize method: @Override public void initialize(ConfigurableWebApplicationContext applicationContext) { final PostgreSQLContainer postgreSQLContainer = PG_CONTAINER.get(); try { if (postgreSQLContainer != null) { // We initialize data source same way as before final DataSource dataSource = initializeDataSource(postgreSQLContainer); applicationContext.getBeanFactory().registerSingleton("dataSource", dataSource); // We initialize liquibase same way as before final Liquibase liquibase = initializeLiquibase(dataSource); applicationContext.getBeanFactory().registerSingleton("liquibase", liquibase); } } catch (LiquibaseException | SQLException e) { // Do something with exception } }
Kako je prikazano u prethodnom primeru koda, inicijalizujemo datasource i Liquibase beanove na isti način kao u prvom primeru, jedina razlika je da ovde eksplicitno stavljamo beanove u kontekst. U našoj test klasi procedura implementacije je slična kao i u primeru sa konfiguracionom test klsom, samo je potrebno u ovom slučaju postaviti našu inicijalizator klasu u @ContextConfiguration anotaciju, na sledeći način:
@ContextConfiguration(initializers = LbAndDsInitializer.class) public class SomeIntRdbsTest Dalja procedura podrazumeva, da nakon što se startuje Postgres container, prosledimo njegovu instancu našoj inicijalizator klasi, koja će konfigurisati i instancirati datasource i Liquibase bean-ove. To se postiže na sledeći način: @ContextConfiguration(initializers = LbAndDsInitializer.class) public class SomeIntRdbsTest { private static final PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer(); @ClassRule public static TestRule exposePortMappings = RuleChain.outerRule(postgreSQLContainer).around(SomeIntRdbsTest::apply); private static Statement apply(Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { LbAndDsInitializer.PG_CONTAINER.set(postgreSQLContainer); base.evaluate(); } }; } }
Sada kada je kontekst pokrenut, postavili smo naše datasource i Liquibase beanove kako treba i možemo da pristupimo databazi u testcontaineru.
Testcontaineri su dobra opcija za brzo postavljanje i podešavanje infrastrukture potrebne za integraciono testiranje, dajući programeru više kontrole nad tim procesom. Postoje specijalizovane opcije containera za različite baze podataka (Postgres, MySQL, Oracle, i Virtuoso), Selenium driver i druge. Ukoliko ovo nije dovoljno, može se koristiti generički container i odgovarajući docker image preuzet ili sa javnih ili sa privatnih docker repozitorija (potrebno je dodatno konfigurisanje u tom slučaju), koji može biti prilagođen specifičnim potrebama testiranja. Kada se koriste JUnit i Spring Test, poželjno je koristiti @ClassRule radi automatskog startovanja, zaustavljanja i uklanjanja containera. Takođe, kao opcije za inicijalizaciju Liquibase i datasource beanova, moguće je iskoristiti ili test configuration klasu ili specijalizovanu klasu za inicijalizaciju aplikativnog konteksta.
Autor teksta je Dragan Torbica, Senior Software consultant u kompaniji BrightMarbles, koja se bavi softver developmentom i konsaltingom.
Objavio/la članak.
petak, 22. Februar, 2019.