Skip to content

Writing Your First Test

Unit tests with Morphium — fast, isolated, no Docker required


Why the InMemory Driver for Tests?

Approach Startup Isolation CI/CD
Testcontainers ~5-10s ✅ Good Requires Docker
Shared test DB Instant ❌ Problems Race conditions
Mocks Instant ✅ Good Tedious to write
InMemory Driver ~50ms ✅ Perfect No Docker needed

The InMemory Driver emulates MongoDB completely in RAM — including aggregation pipelines, change streams, and transactions.


Basic Setup (JUnit 5)

Dependencies

<dependency>
    <groupId>de.caluga</groupId>
    <artifactId>morphium</artifactId>
    <version>6.1.1</version>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
</dependency>

Simple Test

package com.example;

import de.caluga.morphium.Morphium;
import de.caluga.morphium.MorphiumConfig;
import de.caluga.morphium.driver.inmem.InMemoryDriver;
import org.junit.jupiter.api.*;

import static org.junit.jupiter.api.Assertions.*;

class UserTest {

    private Morphium morphium;

    @BeforeEach
    void setUp() {
        MorphiumConfig cfg = new MorphiumConfig();
        cfg.setDatabase("test_" + System.currentTimeMillis()); // Unique DB per test
        cfg.setDriverName(InMemoryDriver.class.getName());
        morphium = new Morphium(cfg);
    }

    @AfterEach
    void tearDown() {
        if (morphium != null) {
            morphium.close();
        }
    }

    @Test
    void shouldStoreAndRetrieveUser() {
        // Given
        User user = new User("testuser", "test@example.com");

        // When
        morphium.store(user);

        // Then
        assertNotNull(user.getId(), "ID should be generated");

        User found = morphium.createQueryFor(User.class)
            .f(User.Fields.username).eq("testuser")
            .get();

        assertNotNull(found);
        assertEquals("testuser", found.getUsername());
        assertEquals("test@example.com", found.getEmail());
    }

    @Test
    void shouldUpdateUser() {
        // Given
        User user = new User("updateme", "old@example.com");
        morphium.store(user);

        // When
        user.setEmail("new@example.com");
        morphium.store(user);

        // Then
        User found = morphium.findById(User.class, user.getId());
        assertEquals("new@example.com", found.getEmail());
    }

    @Test
    void shouldDeleteUser() {
        // Given
        User user = new User("deleteme", "bye@example.com");
        morphium.store(user);

        // When
        morphium.delete(user);

        // Then
        User found = morphium.findById(User.class, user.getId());
        assertNull(found);
    }

    @Test
    void shouldQueryWithConditions() {
        // Given
        morphium.store(new User("alice", "alice@example.com"));
        morphium.store(new User("bob", "bob@example.com"));
        morphium.store(new User("charlie", "charlie@example.com"));

        // When
        long count = morphium.createQueryFor(User.class)
            .f(User.Fields.username).in(java.util.List.of("alice", "bob"))
            .countAll();

        // Then
        assertEquals(2, count);
    }
}

Test Base Class (reusable)

For larger projects: A base class that encapsulates setup/teardown.

package com.example.test;

import de.caluga.morphium.Morphium;
import de.caluga.morphium.MorphiumConfig;
import de.caluga.morphium.driver.inmem.InMemoryDriver;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;

public abstract class MorphiumTestBase {

    protected Morphium morphium;

    @BeforeEach
    void setUpMorphium() {
        MorphiumConfig cfg = new MorphiumConfig();
        cfg.setDatabase("test_" + getClass().getSimpleName() + "_" + System.currentTimeMillis());
        cfg.setDriverName(InMemoryDriver.class.getName());
        morphium = new Morphium(cfg);
    }

    @AfterEach
    void tearDownMorphium() {
        if (morphium != null) {
            morphium.close();
        }
    }

    /**
     * Helper method: Wait until async operations are complete
     */
    protected void waitForAsyncWrites() {
        while (morphium.getWriteBufferCount() > 0) {
            Thread.onSpinWait();
        }
    }
}

Usage:

class OrderServiceTest extends MorphiumTestBase {

    @Test
    void shouldCreateOrder() {
        // morphium is already initialized
        Order order = new Order();
        morphium.store(order);
        // ...
    }
}


Testing Messaging

import de.caluga.morphium.messaging.Messaging;
import de.caluga.morphium.messaging.Msg;
import java.util.concurrent.atomic.AtomicReference;

class MessagingTest extends MorphiumTestBase {

    @Test
    void shouldSendAndReceiveMessage() throws InterruptedException {
        // Given
        Messaging sender = new Messaging(morphium, 100, true);
        Messaging receiver = new Messaging(morphium, 100, true);

        AtomicReference<String> received = new AtomicReference<>();

        receiver.addMessageListener((messaging, msg) -> {
            received.set(msg.getValue());
            return null;
        });

        // When
        sender.sendMessage(new Msg("test-topic", "Hello World", "payload"));

        // Then: Wait for message processing
        Thread.sleep(500);
        assertEquals("payload", received.get());

        // Cleanup
        sender.terminate();
        receiver.terminate();
    }
}

Testing Against Real MongoDB (optional)

Sometimes you want to test against real MongoDB (integration tests):

@Tag("integration")  // Run separately
class MongoDBIntegrationTest {

    private Morphium morphium;

    @BeforeEach
    void setUp() {
        MorphiumConfig cfg = new MorphiumConfig();
        cfg.setDatabase("integration_test");
        cfg.addHostToSeed("localhost:27017");
        morphium = new Morphium(cfg);

        // Clear old data
        morphium.clearCollection(User.class);
    }

    @AfterEach
    void tearDown() {
        if (morphium != null) {
            morphium.clearCollection(User.class);
            morphium.close();
        }
    }

    @Test
    void shouldWorkWithRealMongoDB() {
        // Test against real MongoDB
    }
}

Running tests:

# Unit tests only (InMemory)
mvn test

# Integration tests only
mvn test -Dgroups=integration

# Everything
mvn test -Dgroups="integration,!slow"


Morphium's Test Runner: runtests.sh

Morphium itself has a powerful test runner:

# All tests with InMemory Driver (default)
./runtests.sh

# With real MongoDB
./runtests.sh --driver pooled --uri "mongodb://localhost:27017/test"

# Only specific tags
./runtests.sh --tags core
./runtests.sh --tags messaging

# Parallel
./runtests.sh --parallel 4

# Single class
./runtests.sh --test de.caluga.test.morphium.BasicFunctionalityTests

# Help
./runtests.sh --help

Best Practices

1. Each Test Gets Its Own DB

cfg.setDatabase("test_" + System.currentTimeMillis());
Prevents test interference.

2. Use the Fields Enum

// ✅ Refactoring-safe
.f(User.Fields.username).eq("test")

// ❌ Breaks on rename
.f("username").eq("test")

3. Wait for Async Writes

morphium.store(obj);
// For async writes:
while (morphium.getWriteBufferCount() > 0) {
    Thread.onSpinWait();
}

4. Don't Forget Cleanup

@AfterEach
void cleanup() {
    morphium.close();  // Important!
}

5. InMemory for Unit Tests, Real MongoDB for Integration

@Tag("unit")
class FastUnitTest extends MorphiumTestBase { }

@Tag("integration") 
class SlowIntegrationTest { /* real MongoDB */ }

Troubleshooting

Test Hangs

Probably a messaging listener not terminated:

messaging.terminate();

"Collection not found"

InMemory Driver creates collections lazily. They only exist after the first store().

Flaky Tests

  • Check for race conditions in messaging
  • Use Thread.sleep() or better: CountDownLatch
  • Use unique DB names per test

Next Steps


Questions? → Troubleshooting Guide