marp | theme | paginate | _paginate | transition |
---|---|---|---|---|
true |
rose-pine |
true |
false |
fade |
Javabin Bergen 19/9-2024 Sondre Eikanger Kvalø Sonat Consulting Bergen @zapodot https://github.com/zapodot/test-containers-prez
- Bakgrunn
- Hvordan gjorde vi testing før?
- Fordeler og ulemper
- Testcontainers 101 med eksempler
- Konklusjon
Kilde: Open Container Initiative Image spec v 1.1.0
Kilde: [Docker overview] (https://docs.docker.com/get-started/docker-overview/)
Kode som kjører som en del av standard bygging av et kodeprosjekt som tester ulike utfall som kan gjøres med produksjonskoden
- enhetstest - tester en enkelt funksjon eller klasse. Fokus på f.eks grenseverdier. Mock-er alle avhengigheter
- komponenttest - blackbox testing av en enkelt komponent. Mocker typisk alle eksterne komponenter
- integrasjonstest - tester som fokuserer på grensesnittet mellom egen kode og tredjepart
- Testet ikke integrasjonskode før produksjon
- Testet mot faktisk test/produksjonsmiljø
- Kjørte mocks/stubs som lot deg dekke en del av behovet (f.eks H2Database i kompabilitetsmodus)
@ExtendWith(MockitoExtension.class)
@SuppressWarnings("unchecked")
class RoleReadRepositoryTest {
@Mock
private NamedParameterJdbcOperations namedParameterJdbcOperations;
@InjectMocks
private RoleReadRepository roleReadRepository;
@DisplayName("findById uten treff")
@Test
void findByIdWithoutResult() {
when(namedParameterJdbcOperations.queryForObject(anyString(), anyMap(), isA(RowMapper.class)))
.thenThrow(new IncorrectResultSizeDataAccessException(0));
assertThat(roleReadRepository.findById(Long.MAX_VALUE)).isNull();
verify(namedParameterJdbcOperations).queryForObject(anyString(), anyMap(), isA(RowMapper.class));
}
Tester som fokuserer på en enkelt funksjon eller klasse ved bruk av mocks gir en pekepinn på om koden vår har rett nivå av avhengigheter (coupling).
Testing med Testcontainers er å anse som integrasjonstester og fokuset bør først og fremst være på å sjekke at integrasjonskoden virker mot de tredjeparts avhengighetene vi har i produksjon
- Kan kjøre på
- Docker (Desktop)
- Podman i med docker emulering
- embedded runtime basert på Alpine Linux (eksperimentell)
- Testcontainers cloud
- Trenger ikke ha en container runtime installert
- Pay-as-you-go modell
- Kanskje mest nyttig i CI-sammenheng?
- Etter at Docker Inc kjøpte opp Atomic Jar som laget Testcontainers kan vi anta at det vil henge sammen med deres cloud-løsning
- Kan brukes fra ulike språk og med ulike testrammeverk, f.eks:
- Java (JUnit 4/5 eller Spock)
- Kotlin (kotest)
- .NET
- Go
- Node.js
- Kan kjøre alle containere
- Men: for en del mye brukte containere finnes det egne moduler som gjør jobben litt lettere
- For eksempel finnes det ferdige moduler for de fleste DBMS-systemer og de mest vanlige meldingssystemer
class GenericContainerTest : StringSpec({
val genericContainer = GenericContainer("testcontainers/helloworld:1.1.0")
.withExposedPorts(8080)
.waitingFor(Wait.forHttp("/"))
listeners(genericContainer.perTest())
"Ping skal gi PONG" {
val httpClient = HttpClient(CIO)
httpClient.get("http://${genericContainer.host}:${genericContainer.getMappedPort(8080)}/ping")
.asClue { response ->
response.status.value shouldBe 200
response.readBytes().toString(Charsets.UTF_8) shouldBe "PONG"
}
}
})
@Testcontainers(disabledWithoutDocker = true)
public class ContainerBaseRepositoryTests {
@Container
private PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer("postgres:12.20-alpine");
@Test
void testnoemotdatabasen() {
final var dataSourceConfig = new DataSourceConfig(
postgreSQLContainer.getJdbcUrl(),
postgreSQLContainer.getUsername(),
postgreSQLContainer.getPassword()
//...
}
}
class UserReadRepositoryTest : StringSpec({
val postgres = PostgreSQLContainer("postgres:12.20-alpine")
listeners(postgres.perSpec())
"Test databasekode" {
val exposedConnection = Database.connect(
url = postgres.jdbcUrl,
driver = postgres.driverClassName,
user = postgres.username,
password = postgres.password
)
//...
}
})
try (
Network network = Network.newNetwork();
GenericContainer<?> foo = new GenericContainer<>(TestImages.TINY_IMAGE)
.withNetwork(network)
.withNetworkAliases("foo")
.withCommand(
"/bin/sh",
"-c",
"while true ; do printf 'HTTP/1.1 200 OK\\n\\nyay' | nc -l -p 8080; done"
);
GenericContainer<?> bar = new GenericContainer<>(TestImages.TINY_IMAGE)
.withNetwork(network)
.withCommand("top")
) {
foo.start();
bar.start();
String response = bar.execInContainer("wget", "-O", "-", "http://foo:8080").getStdout();
assertThat(response).as("received response").isEqualTo("yay");
}
- Får testet integrasjonskode mot noe som ligner veldig på det du bruker i produksjon
- Får testet kode som er strenger i kodebasen, f.eks SQL som kompilatoren ikke har et forhold til
- Tar lengre tid å kjøre testene
- Lett for at man ungår å refaktorere koden som integrerer mot tredjepart
- Tester som er avhengig av en tredjepart er ikke enhetstester men integrasjonstester
- Kan velge å skille ut testene som krever containere
- Enhetstester er fremdeles viktig for å sikre at man opprettholder så løs kobling som mulig