Skip to content

Latest commit

 

History

History
259 lines (224 loc) · 8.58 KB

Presentasjon.md

File metadata and controls

259 lines (224 loc) · 8.58 KB
marp theme paginate _paginate transition
true
uncover
true
false
fade

Testcontainers

Presentasjon fagdag 25/10-2024 Sondre Eikanger Kvalø @zapodot https://github.com/zapodot w:200 h:200 drop-shadow


Plan for timen

  1. Om testing
  2. Hvordan gjorde vi testing før?
  3. Testcontainers
  4. Konklusjon

Hva menes med test?

Kode som kjører som en del av standard bygging av et kodeprosjekt som tester ulike utfall som kan gjøres med produksjonskoden


Noen ulike former for tester

  • 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

Hvordan gjorde vi dette før?

  • 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)

Eksempel: enhetstest

@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));
    }

Vi trenger fortsatt enhetstester

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).


Testcontainers

  • Testcontainers gjør det enklere å kjøre containere for å teste som en del av standard bygging
  • Kan kjøre en hvilken som helst container, men har egne moduler for mange ofte brukte containere (DBMS, meldingsbrokere etc)

image


Hva er en container?

width:1000 Kilde: Open Container Initiative Image spec v 1.1.0


Testcontainers

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


Funksjonalitet

  • Kan kjøre på
    • Docker (Desktop)
    • Podman i med docker emulering
    • embedded runtime basert på Alpine Linux (eksperimentell)
    • Testcontainers cloud

Kjøre containere i sky

  • 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

Funksjonalitet

  • Kan brukes fra ulike språk og med ulike testrammeverk, f.eks:
    • Java (JUnit 4/5 eller Spock)
    • Kotlin (kotest)
    • .NET
    • Go
    • Node.js
    • m.fl

Funksjonalitet

  • 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

Eksempel: generisk container 101

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"
            }
    }
})

Eksempel: Bruk av PostgresSQL modul med Java/JUnit 5

@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()
        //...
    }

}

Eksempel: Bruk av PostgresSQL modul med Kotlin/kotest

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
        )
        //...
     }
})

Eksempel: C# med xUnit

public class PostgresDatabaseFixture : IAsyncLifetime
{
    private readonly PostgreSqlContainer _container =
        new PostgreSqlBuilder()
            .WithImage("postgres:12.20-alpine")
            .Build();

    public NpgsqlDataSource DataSource => new DataSourceFactory(_container.GetConnectionString()).Create();

    public Task InitializeAsync()
    {
        return _container.StartAsync().ContinueWith(t => RunMigrations());
    }
    
    public Task DisposeAsync()
        => _container.DisposeAsync().AsTask();
}

Koble sammen containere (code smell?)

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");
}

Fordeler ved å skrive tester som bruker testcontainers

  • 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

Ulemper med å ha tester som bruker testcontainers

  • Tar lengre tid å kjøre testene
  • Lett for at man ungår å refaktorere koden som integrerer mot tredjepart
  • Må ha mulighet for å kjøre containere også i CI-miljø

Konklusjon

  • 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

Takk for meg :-)

bg vertical left w:300 h:300 drop-shadow