Skip to content

Marthym/testy-box

 
 

Repository files navigation

testy-box GitHub license

Quality Gate Status Coverage Maintainability Rating

Description

testy-box is a module providing many extensions for JUnit 5 tests:

  • testy-beat-box provides extensions to run an in-memory Qpid AMQP broker and provide reactive RabbitMQ connections.
  • testy-core-box provides core extensions. All the other projects depend on it.
  • testy-jooq-box provides extensions to run an in-memory H2 database. Test data can be inserted using JOOQ.
  • testy-mongo-box provides extensions to run an embedded MongoDB database. Test data can be inserted.
  • testy-params-box provides aggregators for ParameterizedTest.
  • testy-redis-box provides extensions to run an embedded Redis database.

testy-core-box

This project provides common extensions:

WithObjectMapper

This extension creates and stores an ObjectMapper at step BeforeAll. This mapper can be injected as parameter.

@RegisterExtension
static final WithObjectMapper wObjectMapper = WithObjectMapper
            .builder()
            .addMixin(MyModel.class, MyModelMixin.class)
            .addModule(new ParameterNamesModule())
            .addModule(new JavaTimeModule())
            .build();

@BeforeAll
static void beforeClass(ObjectMapper objectMapper) {
    // (...)
}

ChainedExtension

This extension registers other extensions and runs them:

  • BeforeEach and BeforeAll callbacks are run in the order of the declaration.
  • AfterEach and AfterAll callbacks are run in the reverse order of the declaration.
  • ParameterResolver resolves a type with the first extension able to resolve it. If none can resolve a parameter, the parameter resolution will fail with standard JUnit exception.

This extension is usefull to register test resources in order (for instance, register the DataSource before loading the database schema):

private static final WithInMemoryDatasource wDataSource = WithInMemoryDatasource
        .builder()
        .setTraceLevel(DatabaseTraceLevel.ERROR)
        .setCatalog("my_db_catalog")
        .build();

private static final WithDatabaseLoaded wTestDatabase = WithDatabaseLoaded
        .builder()
        .setDatasourceExtension(wDataSource)
        .build();

@RegisterExtension
static final ChainedExtension chain = ChainedExtension
        .outer(wDataSource)
        .append(wTestDatabase)
        .register();

testy-jooq-box

This project is used to test SQL repositorites. It provides extensions to load an in-memory H2 database, execute SQL scripts with Flyway and insert test data.

WithInMemoryDatasource

This extension creates a named H2 database in memory.

@RegisterExtension
static final WithInMemoryDatasource wDataSource = WithInMemoryDatasource
            .builder()
            .setTraceLevel(DatabaseTraceLevel.ERROR)
            .setCatalog("my_catalog")
            .build();

After this extension has been registered, the DataSource can be injected as parameter.

@BeforeAll
static void beforeClass(DataSource dataSource) {
    // (...)
}

@BeforeEach
void setUp(DataSource dataSource) {
    // (...)
}

If the test registers more than one data source, the parameters shall be annotated with javax.inject.Named to distinct them:

@RegisterExtension
static final WithInMemoryDatasource wDataSource1 = WithInMemoryDatasource
            .builder()
            .setCatalog("my_catalog_1")
            .build();

@RegisterExtension
static final WithInMemoryDatasource wDataSource2 = WithInMemoryDatasource
            .builder()
            .setCatalog("my_catalog_2")
            .build();

@BeforeEach
void setUp(@Named("my_catalog_1") DataSource dataSource1, 
           @Named("my_catalog_2") DataSource dataSource2) {
    // (...)
}

WithDatabaseLoaded

This extension depends on a DatasourceExtension and runs a Flyway migration on the related DB catalog.

By default, the SQL scripts have to be located into db.migration.<catalog> in the classpath, where <catalog> is the name of DataSource catalog. The names of the SQL files shall match Flyway naming convention.

The SQL scripts are run before all the test methods. They are expected to be used to create the database schema.

private static final WithInMemoryDatasource wDataSource = WithInMemoryDatasource
        .builder()
        .setCatalog("my_catalog")
        .build();

// SQL files shall be located in classpath:db.migration.my_catalog
private static final WithDatabaseLoaded wDatabaseLoaded = WithDatabaseLoaded
        .builder()
        .setDatasourceExtension(wDataSource)
        .build();

@RegisterExtension
static final ChainedExtension chain = ChainedExtension
        .outer(wDataSource)
        .append(wDatabaseLoaded)
        .register();

WithDslContext

This extension depends on a DatasourceExtension and creates a JOOQ DSLContext on the related DataSource.

    private static final WithInMemoryDatasource wDataSource = WithInMemoryDatasource
            .builder()
            .setCatalog("my_catalog")
            .build();
    private static final WithDslContext wDsl = WithDslContext
            .builder()
            .setDatasourceExtension(wDataSource)
            .build();

    @RegisterExtension
    static final ChainedExtension chain = ChainedExtension
            .outer(wDataSource)
            .append(wDsl)
            .register();

This DSL can be injected as parameter.

@BeforeEach
void setUp(DSLContext dsl) {
    // (...)
}

If many catalogs are registered, the parameter shall be annotated with javax.inject.Named:

@BeforeEach
void setUp(@Named("my_catalog_1") DSLContext dsl1, 
           @Named("my_catalog_2") DSLContext dsl2) {
    // (...)
}

WithSampleDataLoaded

This extension deletes and inserts test data before each test method.

The test data are inserted as JOOQ records. They can be defined with classes implementing RelationalDataSet

public final class MyElementDataSet implements RelationalDataSet<MyElementRecord> {

    @Override
    public List<MyElementRecord> records() {
        // Return all the records to insert in the table
    }
}

These data set can be added to the extension WithSampleDataLoaded to setup the test data

private static final WithInMemoryDatasource wDataSource = WithInMemoryDatasource
        .builder()
        .setCatalog("my_catalog")
        .build();
private static final WithDatabaseLoaded wDatabaseLoaded = WithDatabaseLoaded
        .builder()
        .setDatasourceExtension(wDataSource)
        .build();
private static final WithDslContext wDSLContext = WithDslContext
        .builder()
        .setDatasourceExtension(wDataSource)
        .build();

private static final WithSampleDataLoaded wSamples = WithSampleDataLoaded
        .builder(wDSLContext)
        .addDataset(new MyElementDataSet())
        .build();

@RegisterExtension
static final ChainedExtension chain = ChainedExtension
        .outer(wDataSource)
        .append(wDatabaseLoaded)
        .append(wDSLContext)
        .append(wSamples)
        .register();

🔥 Only the tables related to the data sets are emptied before each test. If a test inserts rows into another table, this table shall be emptied manually. 🔥

testy-beat-box

This project is used to test classes using RabbitMQ. It provides an extension to run an embedded AMQP broker.

WithRabbitMock

This extension runs an embedded AMQP broker (Qpid by default).

When registered, the Rabbit SenderOptions and ReceiverOptions can be injected as parameters.

@RegisterExtension
static final WithRabbitMock withRabbitMock = WithRabbitMock
            .builder()
            .build();

@BeforeAll
static void beforeClass(SenderOptions senderOptions,
                        ReceiverOptions receiverOptions) {
    // (...)
}

Before each test method, a RabbitMQ Connection and a Channel are opened. They can be injected as parameters.

@RegisterExtension
static final WithRabbitMock withRabbitMock = WithRabbitMock
            .builder()
            .build();

@BeforeEach
void setUp(Connection connection,
           Channel channel) {
    // (...)
}

Queues and exchanges can also be created with the extension. When declaring a queue and an exchange, they are bound together with an empty routing key.

@RegisterExtension
static final WithRabbitMock withRabbitMock = WithRabbitMock
            .builder()
            .declareQueueAndExchange("my_queue", "my_exchange")
            .build();

The queues are deleted automatically by closing the connection after each test method.

In order to simplify mocking of rabbit queues, Mocked sender and receiver can be injected to the test:

These mocks use AmqpMessage as requests/responses. AmqpMessage can define the body and the headers of the message.

@RegisterExtension
static final WithRabbitMock withRabbitMock = WithRabbitMock
            .builder()
            .declareQueueAndExchange("my_queue", "my_exchange")
            .build();

@Test
void myTest(MockedSender mockedSender) {
    final AmqpMessage request = AmqpMessage.of("test-request".getBytes());
    final String routingKey = "";

    // Simply publish a message on the tested queue
    mockedSender.basicPublish(request)
                .on("my_exchange", routingKey);

    // Send a RPC request
    final Mono<Delivery> response = mockedSender.rpc(request)
                .on("my_exchange", routingKey);
    // (...)
}
@RegisterExtension
static final WithRabbitMock withRabbitMock = WithRabbitMock
            .builder()
            .declareQueueAndExchange("my_queue", "my_exchange")
            .build();

@Test
void myTest(MockedReceiver mockedReceiver) {
    final AmqpMessage response = AmqpMessage.of("test-response".getBytes());

    // To consume more than one message, use consume(int)
    final Flux<Delivery> requests = mockedReceiver.consumeOne()
                  .on("my_queue")
                  .thenRespond(response)
                  .start();
    // (...)
}