Wzorzec ObjectMother

Wprowadzenie

Wzorzec projektowy ObjectMother to technika używana w testach jednostkowych, która pomaga w generowaniu obiektów testowych. W tym wpisie przedstawię, jak można zastosować ten wzorzec w języku Kotlin.

Zalety

  1. Czytelność kodu: Dzięki scentralizowaniu generowania testowych obiektów kod jest bardziej czytelny.
  2. Łatwość aktualizacji: Jeśli obiekty zmieniają się, wystarczy zmienić je w jednym miejscu.
  3. Eliminacja duplikacji kodu: Unikanie wielokrotnego tworzenia tych samych obiektów w różnych testach.

Wady

  1. Złożoność: Może wprowadzić dodatkową złożoność, jeśli obiekty są bardzo rozbudowane.
  2. Naruszenie izolacji testów: Ryzyko, że zmiana w ObjectMother wpłynie na wiele testów naraz.

Stosowalność

Wzorzec najlepiej sprawdza się w dużych projektach z wieloma testami, które używają podobnych obiektów.

Przykład

Obiekty używane w przykładzie

data class Customer(val id: Int, val name: String, val email: String)
data class Product(val id: Int, val name: String, val price: Double)
data class Order(val id: Int, val customer: Customer, val products: List<Product>)

Pobierz kod na GitHub

ObjectMother w Kotlinie

package pro.adamski.objectMother

import kotlin.random.Random

object ObjectMother {
    fun customer(
        id: Int = Random.nextInt(),
        name: String = "John Doe${Random.nextInt()}",
        email: String = "info-${Random.nextInt()}@example.com"
    ): Customer {
        return Customer(id, name, email)
    }

    fun product(
        id: Int = Random.nextInt(),
        name: String = "The ${Random.nextInt()}th laptop",
        price: Double = Random.nextDouble(1000.0, 5000.0)
    ): Product {
        return Product(id, name, price)
    }

    fun order(
        id: Int = Random.nextInt(),
        customer: Customer = customer(),
        products: List<Product> = listOf(product())
    ): Order {
        return Order(id, customer, products)
    }

    fun largeOrder(id: Int = Random.nextInt(), customer: Customer = customer()): Order {
        val products = List(100) { product(id = it, name = "Product$it") }
        return Order(id, customer, products)
    }
}

Pełny kod na GitHub

ObjectMother w Javie

package pro.adamski.objectMother;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class ObjectMotherJava {
    private static final Random random = new Random();

    public static CustomerBuilder customer() {
        return new CustomerBuilder()
                .withId(random.nextInt())
                .withName("John Doe" + random.nextInt())
                .withEmail("info-" + random.nextInt() + "@example.com");
    }

    public static ProductBuilder product() {
        return new ProductBuilder()
                .withId(random.nextInt())
                .withName("The " + random.nextInt() + "th laptop")
                .withPrice(random.nextDouble() * (5000.0 - 1000.0) + 1000.0);
    }

    public static OrderBuilder order() {
        return new OrderBuilder()
                .withId(random.nextInt())
                .withCustomer(customer().build())
                .addProduct(product().build());
    }

    public static Order largeOrder() {
        OrderBuilder orderBuilder = new OrderBuilder()
                .withId(random.nextInt())
                .withCustomer(customer().build());

        for (int i = 0; i < 100; i++) {
            orderBuilder.addProduct(product().withId(i).withName("Product" + i).build());
        }
        return orderBuilder.build();
    }
}

Pełny kod fluent builderami na GitHub

Testy w JUnit 5

package pro.adamski.objectMother

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

class OrderTest {

    @Test
    fun testCustomOrder() {
        // Given
        val customProduct = ObjectMother.product(name = "Custom Laptop", price = 2000.0)

        // When
        val customOrder = ObjectMother.order(products = listOf(customProduct))

        // Then
        assertOrderProduct(customOrder.products.first(), "Custom Laptop", 2000.0)
    }

    @Test
    fun testRandomLargeOrder() {
        // When
        val largeOrder = ObjectMother.largeOrder()

        // Then
        assertOrderProducts(largeOrder, 100, 1000.0..5000.0)
    }

    @Test
    fun testCustomOrderJava() {
        // Given
        val customProduct = ObjectMotherJava.product().withName("Custom Laptop").withPrice(2000.0).build()

        // When
        val customOrder = ObjectMotherJava.order().withProducts(listOf(customProduct)).build()

        // Then
        assertOrderProduct(customOrder.products.first(), "Custom Laptop", 2000.0)
    }

    @Test
    fun testRandomLargeOrderJava() {
        // When
        val largeOrder = ObjectMotherJava.largeOrder()

        // Then
        assertOrderProducts(largeOrder, 100, 1000.0..5000.0)
    }

    private fun assertOrderProducts(
        largeOrder: Order,
        numberOfProducts: Int,
        priceRange: ClosedFloatingPointRange<Double>
    ) {
        assertThat(largeOrder.products).hasSize(numberOfProducts)
        assertThat(largeOrder.products).allMatch {
            it.price in priceRange
        }
    }

    private fun assertOrderProduct(firstProduct: Product, name: String, price: Double) {
        assertThat(firstProduct.name).isEqualTo(name)
        assertThat(firstProduct.price).isEqualTo(price)
    }
}

Pełny kod na GitHub

Podsumowanie

Wzorzec ObjectMother jest bardzo użyteczny, zwłaszcza w dużych projektach z rozbudowaną bazą testów. Pomaga w utrzymaniu czystego i efektywnego kodu testowego. W przypadku implementacji w Javie zastosowanie fluent builderów pozwala na jeszcze większą czytelność kodu testowego.

Bibliografia

  1. ObjectMother
  2. Combining Object Mother and Fluent Builder for the Ultimate Test Data Factory