BDD rather than TDD: Result-Oriented Testing

Yaser Al-Najjar - Jan 4 '19 - - Dev Community

TDD doesn't click, give me the solution

I've talked before about TDD (Test Driven Development), and how you end up testing your framework instead of the logic of your app.

In case you haven't read the previous topic, you can find it here: https://dev.to/0xrumple/when-tdd-doesnt-click-something-else-should-click-harder-2h9h

BDD (Behavior Driven Development) to the rescue

This time, I decided I will do testing no matter what, so I chose an approach that doesn't focus on mocks, fakes, interfaces, dependency injection... but it's all about checking the outcomes, and that's what BDD is for: BEHAVIOR.

Did I try it?

Yes, I didn't allow myself to write this post until we (me and my creative friend @alhakem) finished this dormitory management system (where students can search/reserve rooms and managers can approve/reject reservations and manage their dorms):

demo

Here is the whole project, "features" folder contain the tests: https://github.com/coretabs/dorm-portal
Feel free to PR or create issues.

Fun facts

I worked on a bit complex filtering engine (where there are multiCheckbox/singleCheckbox/integer filtering criteria), and with BDD I finished it in about one week... all tests green, woohoo!

The whole project was done in about two months, and... all green 😉

How can I do BDD?

It's really simple:

1. Write an acceptance criteria (scenario)

The common way is using Gherkin language, now don't get scared cuz it's really intuitive, just look at this sample (should be self explanatory):

Feature: Reservation

    Scenario: As a student
              I want to make reservations
              So that I get a room to stay in

        Given we have 2 dormitories (and 1 room each available to reserve)
        Given two students who reserved nothing

        When create a reservation
        Then quota of the room should decrease
Enter fullscreen mode Exit fullscreen mode

2. Implement the steps

Depending on your language/framework of choice this might differ; I'm using django (with python of course), and there is a great way of doing it using behave-django package.

Here are the above mentioned acceptance criteria implemented:

from behave import given, when, then
from api.engine.models import RoomCharacteristics
from features.steps.factory import (create_alfam_dovec_with_4_rooms,
                                    create_student,
                                    create_reservation)


@given('we have 2 dormitories (and 1 room each available to reserve)')
def arrange(context):
    create_alfam_dovec_with_4_rooms(context)


@given('two students who reserved nothing')
def arrange(context):
    context.user1 = create_student(context, 'Owen')
    context.user2 = create_student(context, 'Tia')


@when('create a reservation')
def act(context):
    context.previous_quota = context.room1.allowed_quota
    context.reservation1 = create_reservation(context.room1, context.user1)


@then('quota of the room should decrease')
def test(context):
    context.room1 = RoomCharacteristics.objects.get(pk=context.room1.id)
    assert context.room1.allowed_quota == context.previous_quota - 1
Enter fullscreen mode Exit fullscreen mode

3. Make them green

Now comes the part of writing your actual code, and the goal is to pass that quota test.

from django.db import (models as django_models, transaction)
from .exceptions import NoEnoughQuotaException


class Dormitory(django_models.Model):
    name = django_models.CharField(max_length=60)


class RoomCharacteristics(django_models.Model):
    allowed_quota = django_models.PositiveIntegerField(default=0)

    dormitory = django_models.ForeignKey(
        Dormitory, related_name='room_characteristics', on_delete=django_models.CASCADE)

    def decrease_quota(self):
        if self.allowed_quota == 0:
            raise NoEnoughQuotaException()

        self.allowed_quota -= 1


class Reservation(django_models.Model):
    user = django_models.ForeignKey(
        User, related_name='reservations', on_delete=django_models.CASCADE)

    room_characteristics = django_models.ForeignKey(
        RoomCharacteristics, related_name='reservations', on_delete=django_models.CASCADE)

    @classmethod
    def create(cls, *args, **kwargs):
        room_characteristics = kwargs['room_characteristics']
        result = cls(*args, **kwargs)

        with transaction.atomic():
            room_characteristics.decrease_quota()
            result.save()
            room_characteristics.save()

        return result
Enter fullscreen mode Exit fullscreen mode

Seriously?! That sounds tedious!

Nah! believe me, cuz I worked on other projects where adding features or changing one simple thing might break many other parts in your project, it literally becomes like:

drawing
image source: giphy

You will realize that once you add X feature and the customers come to you shouting: "registration doesn't work"... IT SUCKS TO CODE WITHOUT TESTING.

When to do BDD vs TDD?

doing-bdd-tdd

References

  1. Gherkin language: https://docs.cucumber.io/gherkin/reference/
  2. BDD course on Pluralsight: https://www.pluralsight.com/courses/pragmatic-bdd-dotnet
  3. Different types of testing explained: https://dev.to/thejessleigh/different-types-of-testing-explained-1ljo
. . . . . . . . . . . . . . . .
Terabox Video Player