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):
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
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
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
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:
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?
References
- Gherkin language: https://docs.cucumber.io/gherkin/reference/
- BDD course on Pluralsight: https://www.pluralsight.com/courses/pragmatic-bdd-dotnet
- Different types of testing explained: https://dev.to/thejessleigh/different-types-of-testing-explained-1ljo