Skip to content

MongoDB

    dependencies {
        testImplementation("com.trendyol:stove-testing-e2e-mongodb:$version")
    }

Configure

After getting the library from the maven source, while configuring TestSystem you will have access to mongodb function. This function configures the MongoDB Docker container that is going to be started.

TestSystem()
  .with {
    mongodb {
      MongodbSystemOptions(
        configureExposedConfiguration = { cfg ->
          listOf(
            "mongodb.uri=${cfg.connectionString}",
            "mongodb.host=${cfg.host}",
            "mongodb.port=${cfg.port}"
          )
        }
      )
    }
  }
  .run()

Container Options

Customize the MongoDB container:

TestSystem()
  .with {
    mongodb {
      MongodbSystemOptions(
        container = MongoContainerOptions(
          registry = "docker.io",
          image = "mongo",
          tag = "6.0",
          containerFn = { container ->
            // Additional container configuration
            container.withEnv("MONGO_INITDB_DATABASE", "testdb")
          }
        ),
        configureExposedConfiguration = { cfg ->
          listOf(
            "mongodb.uri=${cfg.connectionString}",
            "mongodb.host=${cfg.host}",
            "mongodb.port=${cfg.port}"
          )
        }
      )
    }
  }
  .run()

Database Options

Configure the default database and collection:

TestSystem()
  .with {
    mongodb {
      MongodbSystemOptions(
        databaseOptions = DatabaseOptions(
          default = DatabaseOptions.DefaultDatabase(
            name = "myDatabase",
            collection = "myCollection"
          )
        ),
        configureExposedConfiguration = { cfg ->
          listOf(
            "mongodb.uri=${cfg.connectionString}"
          )
        }
      )
    }
  }
  .run()

Custom Client Configuration

Customize the MongoDB client settings:

TestSystem()
  .with {
    mongodb {
      MongodbSystemOptions(
        configureClient = { settings ->
          settings.applyToConnectionPoolSettings { pool ->
            pool.maxSize(10)
            pool.minSize(1)
          }
          settings.applyToSocketSettings { socket ->
            socket.connectTimeout(10, TimeUnit.SECONDS)
            socket.readTimeout(30, TimeUnit.SECONDS)
          }
        },
        configureExposedConfiguration = { cfg ->
          listOf("mongodb.uri=${cfg.connectionString}")
        }
      )
    }
  }
  .run()

Custom Serialization

Configure custom serialization for your documents:

TestSystem()
  .with {
    mongodb {
      val customSerde = StoveSerde.jackson.anyJsonStringSerde(
        StoveSerde.jackson.byConfiguring {
          disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
          enable(MapperFeature.DEFAULT_VIEW_INCLUSION)
          registerModule(JavaTimeModule())
          registerModule(KotlinModule.Builder().build())
        }
      )

      MongodbSystemOptions(
        serde = customSerde,
        configureExposedConfiguration = { cfg ->
          listOf("mongodb.uri=${cfg.connectionString}")
        }
      )
    }
  }
  .run()

Migrations

Stove provides a way to run migrations before tests start:

class CreateIndexesMigration : DatabaseMigration<MongodbMigrationContext> {
  override val order: Int = 1

  override suspend fun execute(connection: MongodbMigrationContext) {
    val db = connection.client.getDatabase(connection.options.databaseOptions.default.name)

    // Create indexes
    db.getCollection<Document>("users").createIndex(
      Indexes.ascending("email"),
      IndexOptions().unique(true)
    )

    db.getCollection<Document>("products").createIndex(
      Indexes.compoundIndex(
        Indexes.ascending("category"),
        Indexes.descending("createdAt")
      )
    )
  }
}

Register migrations in your TestSystem configuration:

TestSystem()
  .with {
    mongodb {
      MongodbSystemOptions(
        configureExposedConfiguration = { cfg ->
          listOf("mongodb.uri=${cfg.connectionString}")
        }
      ).migrations {
        register<CreateIndexesMigration>()
      }
    }
  }
  .run()

Usage

Saving Documents

Save documents to MongoDB collections:

data class User(
  val id: String,
  val name: String,
  val email: String,
  val age: Int
)

TestSystem.validate {
  mongodb {
    val userId = ObjectId().toHexString()

    // Save to default collection
    save(
      instance = User(id = userId, name = "John Doe", email = "john@example.com", age = 30),
      objectId = userId
    )

    // Save to specific collection
    save(
      instance = User(id = userId, name = "Jane Doe", email = "jane@example.com", age = 28),
      objectId = userId,
      collection = "users"
    )
  }
}

Getting Documents

Retrieve and validate documents by ObjectId:

TestSystem.validate {
  mongodb {
    val userId = ObjectId().toHexString()

    // First save the document
    save(
      instance = User(id = userId, name = "John Doe", email = "john@example.com", age = 30),
      objectId = userId,
      collection = "users"
    )

    // Get from specific collection
    shouldGet<User>(objectId = userId, collection = "users") { user ->
      user.id shouldBe userId
      user.name shouldBe "John Doe"
      user.email shouldBe "john@example.com"
      user.age shouldBe 30
    }
  }
}

Checking Non-Existence

Verify that documents don't exist:

TestSystem.validate {
  mongodb {
    val nonExistentId = ObjectId().toHexString()

    // Check default collection
    shouldNotExist(objectId = nonExistentId)

    // Check specific collection
    shouldNotExist(objectId = nonExistentId, collection = "users")
  }
}

Deleting Documents

Delete documents and verify deletion:

TestSystem.validate {
  mongodb {
    val userId = ObjectId().toHexString()

    // Save a document
    save(
      instance = User(id = userId, name = "John Doe", email = "john@example.com", age = 30),
      objectId = userId,
      collection = "users"
    )

    // Delete it
    shouldDelete(objectId = userId, collection = "users")

    // Verify deletion
    shouldNotExist(objectId = userId, collection = "users")
  }
}

Querying Documents

Query documents using MongoDB query syntax:

TestSystem.validate {
  mongodb {
    // Setup test data
    listOf(
      User(id = ObjectId().toHexString(), name = "Alice", email = "alice@example.com", age = 25),
      User(id = ObjectId().toHexString(), name = "Bob", email = "bob@example.com", age = 35),
      User(id = ObjectId().toHexString(), name = "Charlie", email = "charlie@example.com", age = 28)
    ).forEach { user ->
      save(instance = user, objectId = ObjectId().toHexString(), collection = "users")
    }

    // Simple query
    shouldQuery<User>(
      query = """{ "age": { "${'$'}gte": 30 } }""",
      collection = "users"
    ) { users ->
      users.size shouldBe 1
      users.first().name shouldBe "Bob"
    }

    // Query with multiple conditions
    shouldQuery<User>(
      query = """
        {
          "${'$'}and": [
            { "age": { "${'$'}gte": 25 } },
            { "age": { "${'$'}lte": 30 } }
          ]
        }
      """.trimIndent(),
      collection = "users"
    ) { users ->
      users.size shouldBe 2
      users.map { it.name } shouldContainAll listOf("Alice", "Charlie")
    }
  }
}

Accessing the Client Directly

For advanced operations, access the MongoDB client:

TestSystem.validate {
  mongodb {
    val mongoClient = client()

    // Access the database
    val db = mongoClient.getDatabase("myDatabase")

    // List collections
    val collections = db.listCollectionNames().toList()

    // Perform custom operations
    db.getCollection<Document>("users")
      .find()
      .limit(10)
      .toList()
      .also { documents ->
        documents.size shouldBeLessThanOrEqual 10
      }
  }
}

Pause and Unpause Container

Control the MongoDB container for testing failure scenarios:

TestSystem.validate {
  mongodb {
    val userId = ObjectId().toHexString()

    // MongoDB is running
    save(
      instance = User(id = userId, name = "John", email = "john@example.com", age = 30),
      objectId = userId,
      collection = "users"
    )

    // Pause the container
    pause()

    // Your application should handle the failure
    // ...

    // Unpause the container
    unpause()

    // Verify recovery
    shouldGet<User>(objectId = userId, collection = "users") { user ->
      user.name shouldBe "John"
    }
  }
}

Warning

pause(), unpause(), and inspect() operations are not supported when using a provided instance.

Container Inspection

Inspect the MongoDB container:

TestSystem.validate {
  mongodb {
    val info = inspect()
    info?.let {
      println("Container ID: ${it.containerId}")
      println("Network: ${it.network}")
      println("IP Address: ${it.ipAddress}")
    }
  }
}

Complete Example

Here's a complete end-to-end test combining HTTP, MongoDB, and Kafka:

data class Product(
  val id: String,
  val name: String,
  val description: String,
  val price: Double,
  val categoryId: Int,
  val stock: Int,
  val createdAt: Instant = Instant.now()
)

test("should create product and store in mongodb") {
  TestSystem.validate {
    val productId = ObjectId().toHexString()
    val productName = "Gaming Laptop"
    val categoryId = 1

    // Mock external service
    wiremock {
      mockGet(
        url = "/categories/$categoryId",
        statusCode = 200,
        responseBody = Category(id = categoryId, name = "Electronics", active = true).some()
      )
    }

    // Create product via API
    http {
      postAndExpectBody<ProductResponse>(
        uri = "/products",
        body = ProductCreateRequest(
          name = productName,
          description = "High-performance gaming laptop",
          price = 1299.99,
          categoryId = categoryId,
          stock = 10
        ).some()
      ) { response ->
        response.status shouldBe 201
        response.body().id shouldNotBe null
      }
    }

    // Verify stored in MongoDB
    mongodb {
      shouldQuery<Product>(
        query = """{ "name": "$productName" }""",
        collection = "products"
      ) { products ->
        products.size shouldBe 1
        products.first().also { product ->
          product.name shouldBe productName
          product.price shouldBe 1299.99
          product.categoryId shouldBe categoryId
          product.stock shouldBe 10
        }
      }
    }

    // Verify event was published
    kafka {
      shouldBePublished<ProductCreatedEvent>(atLeastIn = 10.seconds) {
        actual.name == productName &&
        actual.price == 1299.99
      }
    }

    // Update product stock via API
    http {
      putAndExpectBodilessResponse(
        uri = "/products/$productId/stock",
        body = UpdateStockRequest(quantity = -2).some()
      ) { response ->
        response.status shouldBe 200
      }
    }

    // Verify stock updated in MongoDB
    mongodb {
      shouldQuery<Product>(
        query = """{ "name": "$productName" }""",
        collection = "products"
      ) { products ->
        products.first().stock shouldBe 8
      }
    }
  }
}

Integration with Application

Verify application behavior using the bridge:

test("should use repository to save product") {
  TestSystem.validate {
    val productId = ObjectId().toHexString()
    val product = Product(
      id = productId,
      name = "Test Product",
      description = "Test Description",
      price = 99.99,
      categoryId = 1,
      stock = 5
    )

    // Use application's repository
    using<ProductRepository> {
      save(product)
    }

    // Verify in MongoDB
    mongodb {
      shouldQuery<Product>(
        query = """{ "name": "Test Product" }""",
        collection = "products"
      ) { products ->
        products.size shouldBe 1
        products.first().id shouldBe productId
        products.first().price shouldBe 99.99
      }
    }
  }
}

Advanced Operations

Aggregation Queries

TestSystem.validate {
  mongodb {
    val mongoClient = client()
    val db = mongoClient.getDatabase("myDatabase")

    // Aggregation pipeline
    val pipeline = listOf(
      Aggregates.match(Filters.gte("price", 100)),
      Aggregates.group("${'$'}categoryId", 
        Accumulators.sum("totalProducts", 1),
        Accumulators.avg("avgPrice", "${'$'}price")
      ),
      Aggregates.sort(Sorts.descending("totalProducts"))
    )

    db.getCollection<Document>("products")
      .aggregate(pipeline)
      .toList()
      .also { results ->
        results.size shouldBeGreaterThan 0
        // Each result has categoryId, totalProducts, and avgPrice
      }
  }
}

Bulk Operations

TestSystem.validate {
  mongodb {
    val mongoClient = client()
    val db = mongoClient.getDatabase("myDatabase")
    val collection = db.getCollection<Document>("users")

    // Bulk insert
    val users = (1..100).map { i ->
      Document()
        .append("_id", ObjectId())
        .append("name", "User $i")
        .append("email", "user$i@example.com")
        .append("age", 20 + (i % 50))
    }

    collection.insertMany(users)

    // Bulk update
    collection.updateMany(
      Filters.gte("age", 40),
      Updates.set("status", "senior")
    )

    // Verify
    val seniorCount = collection.countDocuments(Filters.eq("status", "senior"))
    seniorCount shouldBeGreaterThan 0
  }
}

Transaction Support

TestSystem.validate {
  mongodb {
    val mongoClient = client()

    mongoClient.startSession().use { session ->
      session.startTransaction()
      try {
        val db = mongoClient.getDatabase("myDatabase")

        // Perform operations in transaction
        db.getCollection<Document>("accounts")
          .updateOne(
            session,
            Filters.eq("accountId", "sender"),
            Updates.inc("balance", -100.0)
          )

        db.getCollection<Document>("accounts")
          .updateOne(
            session,
            Filters.eq("accountId", "receiver"),
            Updates.inc("balance", 100.0)
          )

        session.commitTransaction()
      } catch (e: Exception) {
        session.abortTransaction()
        throw e
      }
    }
  }
}

Working with Indexes

TestSystem.validate {
  mongodb {
    val mongoClient = client()
    val db = mongoClient.getDatabase("myDatabase")
    val collection = db.getCollection<Document>("users")

    // Create unique index
    collection.createIndex(
      Indexes.ascending("email"),
      IndexOptions().unique(true)
    )

    // Create compound index
    collection.createIndex(
      Indexes.compoundIndex(
        Indexes.ascending("status"),
        Indexes.descending("createdAt")
      )
    )

    // Create text index for search
    collection.createIndex(
      Indexes.text("name")
    )

    // List indexes
    collection.listIndexes().toList().also { indexes ->
      indexes.size shouldBeGreaterThan 1
    }
  }
}

Provided Instance (External MongoDB)

For CI/CD pipelines or shared infrastructure:

TestSystem()
  .with {
    mongodb {
      MongodbSystemOptions.provided(
        connectionString = System.getenv("MONGODB_URI") ?: "mongodb://localhost:27017",
        host = System.getenv("MONGODB_HOST") ?: "localhost",
        port = System.getenv("MONGODB_PORT")?.toInt() ?: 27017,
        cleanup = { client ->
          // Clean up test data after tests
          client.getDatabase("testdb").drop()
        },
        configureExposedConfiguration = { cfg ->
          listOf(
            "mongodb.uri=${cfg.connectionString}",
            "mongodb.host=${cfg.host}",
            "mongodb.port=${cfg.port}"
          )
        }
      )
    }
  }
  .run()

Error Handling

TestSystem.validate {
  mongodb {
    // Document not found
    val nonExistentId = ObjectId().toHexString()
    shouldNotExist(objectId = nonExistentId, collection = "users")

    // Attempting to get non-existent document throws exception
    assertThrows<NoSuchElementException> {
      shouldGet<User>(objectId = nonExistentId, collection = "users") { }
    }

    // Verify existence check on existing document
    val existingId = ObjectId().toHexString()
    save(
      instance = User(id = existingId, name = "Existing", email = "existing@example.com", age = 25),
      objectId = existingId,
      collection = "users"
    )

    assertThrows<AssertionError> {
      shouldNotExist(objectId = existingId, collection = "users")
    }
  }
}

Working with ObjectId

MongoDB uses ObjectId as the default identifier. Stove handles this transparently:

data class UserWithStringId(
  val id: String, // String representation of ObjectId
  val name: String,
  val email: String
)

TestSystem.validate {
  mongodb {
    // Generate ObjectId
    val objectId = ObjectId()
    val stringId = objectId.toHexString()

    // Save with string ID
    save(
      instance = UserWithStringId(id = stringId, name = "Test", email = "test@example.com"),
      objectId = stringId,
      collection = "users"
    )

    // Retrieve using string ID
    shouldGet<UserWithStringId>(objectId = stringId, collection = "users") { user ->
      user.id shouldBe stringId
      user.name shouldBe "Test"
    }
  }
}