Refactoring to BDD-like tests

Sometimes I found myself in a situation where it’s hard to understand what some components suppose to achieve. In these situations, I go to test where scenarios/contracts of this component should be.
But then I find some integrations tests with some entries in DB and expected mechanical results.
My strategy in this situation is to refactor test from integration test to BDD -like tests. Try to be as close to Gherkin scenarios using commands and events to observe this component (avoiding direct DB connection).

Here you have an example of such a situation. Before and after of such tests. After I tried to describe what I found out during refactoring.

Before / Original tests

class TestUpdateStock:
    @fixture(autouse=True)
    def setUp(self, container):
        self.repository = container.get(MysqlProductRepository)

    def test_apply_por_order(self, handler, por_command, dto, in_sync):
        self.repository.save(dto)
        event = handler.handle(por_command)
        assert event.sku == dto.sku
        assert event.affected == [
            StockIdentifier(site=stock.site, platform=stock.platform)
            for stock in dto.stocks
        ]

    def test_apply_por_order_before_update(
            self, handler, por_command_before_update, dto, in_sync):
        self.repository.save(dto)
        event = handler.handle(por_command_before_update)
        assert event is None

    def test_apply_in_sync_order(self, handler, in_sync_command, dto, in_sync):
        self.repository.save(dto)
        event = handler.handle(in_sync_command)
        assert event.sku == dto.sku
        assert event.affected == [
            StockIdentifier(site=stock.site, platform=stock.platform)
            for stock in dto.stocks
        ]

    @fixture
    def in_sync(self):
        return StockDTO(
            platform=Platform.Other,
            site='DE',
            quantity=Quantity(5),
        )

    @fixture
    def dto(self, in_sync):
        stock = StockDTO(
            platform=Platform.Some,
            site='PL',
            quantity=Quantity(5),
        )
        dto = ProductDTOFactory(
            stocks=[stock, in_sync], last_update=datetime.utcnow())
        dto.point_of_reference = stock
        return dto

    @fixture
    def in_sync_command(self, dto):
        return ApplyOrder(
            sku=dto.sku,
            platform=dto.point_of_reference.platform,
            site=dto.point_of_reference.site,
            quantity_sold=Quantity(10),
            when_sold=datetime.utcnow() + timedelta(minutes=10),
        )

    @fixture
    def por_command(self, dto):
        return ApplyOrder(
            sku=dto.sku,
            platform=dto.in_sync[0].platform,
            site=dto.in_sync[0].site,
            quantity_sold=Quantity(1),
            when_sold=datetime.utcnow(),
        )

    @fixture
    def por_command_before_update(self, dto):
        return ApplyOrder(
            sku=dto.sku,
            platform=dto.point_of_reference.platform,
            site=dto.point_of_reference.site,
            quantity_sold=Quantity(1),
            when_sold=datetime.utcnow() - timedelta(days=1),
        )

After / BDD-like tests

class TestUpdateStock:
    @fixture(autouse=True)
    def setUp(self, handler):
        self.stocks = handler

        with given('Registered product with point of reference stock'):
            self.point_of_reference = StockIdentifier()
            self.sku = handler.handle(
                RegisterProduct(stock=self.point_of_reference)
            ).sku

        with given('Registered product stock in sync'):
            self.in_sync = StockIdentifier()
            self.stocks.handle(
                RegisterStock(sku=self.sku, stock=self.in_sync)
            )

    def test_affects_only_in_sync_stocks_when_order_on_por(self):
        old_quantity = self.quantity()

        with given('Order on point of reference'):
            order = self.order(self.point_of_reference)
            result = self.stocks.handle(order)
            assert result.sku == self.sku

        with expect('Affects only stocks in sync'):
            affected, = result.affected
            assert affected.identifier == self.in_sync
            assert affected.change == QuantityChange(
                previous=old_quantity,
                current=old_quantity - order.quantity_sold,
            )

    def test_ignore_order_made_before_seller_changed_por_stock(self):
        with given('Not processed order'):
            order = self.order(self.point_of_reference)

        with when('Seller updates stocks'):
            seller_updated_stock = UpdateStock(
                sku=self.sku,
                stock=self.point_of_reference,
                when_updated=order.when_sold + timedelta(days=1),
            )
            assert self.stocks.handle(seller_updated_stock)

        with then('order made before update is ignored'):
            assert not self.stocks.handle(order)

    def test_affects_all_stocks_when_order_made_on_in_sync_stock(self):
        old_quantity = self.quantity()

        with given('Order on stock in sync'):
            order = self.order(self.in_sync)
            result = self.stocks.handle(order)
            assert result.sku == self.sku

        with expect('Affects all stocks'):
            affected = [a.identifier for a in result.affected]
            assert self.point_of_reference in affected
            assert self.in_sync in affected
            assert all(
                affected.change == QuantityChange(
                    previous=old_quantity,
                    current=old_quantity - order.quantity_sold,
                )
                for affected in result.affected
            )

    def test_apply_order_only_once_when_send_multiple_times(self):
        with given('Processed point of reference order'):
            por_order = self.order(self.point_of_reference)
            assert self.stocks.handle(por_order)

        with expect('Cannot be applied anymore'):
            assert not self.stocks.handle(por_order)
            assert not self.stocks.handle(por_order)
            assert not self.stocks.handle(por_order)

        with given('Processed stock in sync order'):
            in_sync_order = self.order(self.in_sync)
            assert self.stocks.handle(in_sync_order)

        with expect('Cannot be applied anymore'):
            assert not self.stocks.handle(in_sync_order)
            assert not self.stocks.handle(in_sync_order)
            assert not self.stocks.handle(in_sync_order)

    def order(self, stock: StockIdentifier) -> ApplyOrder:
        return ApplyOrder(sku=self.sku, stock=stock)

    def quantity(self) -> Quantity:
        return self.stocks.get_quantity(sku=self.sku, stock=self.por)

Hi there 👋
It’s nice to meet you.

Sign up to join my mailing list.

I don’t spam!