I recently attended the PHP UK 2017 conference in London. Besides finding out that PHP makes up 82% of the web, I also saw an amazing talk by Ciaran McNulty (@ciaranmcnulty) called Driving Design through Examples (the slides are a good reference point, and you can also see a video of the talk).
Note: Although this article specifically refers to PHP, the principles can be applied to any kind of development (just substitute in the appropriate tools).
Disclaimer: This is a summary of the talk, with some of my own personal interpretations and thoughts. Any mistakes are my own.
“BDD is the art of using examples in conversations to illustrate behaviour” — Liz Keogh.
When you’re scoping out a new project, you often have a bunch of rules or business requirements that you have to adhere to. However, rules are ambiguous and often lead to assumptions that can cause delays in a project when you have to go back to fix them. Examples, however, are unambiguous and testable; they form the basis of user stories and scenarios. When you receive your set of rules for a project, sit down with the relevant people — a business expert, a testing expert and a development expert — to discuss the feature together and come up with the examples.
Let’s give an example. Well, actually, lets just steal Ciaran’s example…
- Every £1 spent on a flight earns 1 point
- 100 points can be redeemed for £10 off a future flight
- Flights are taxed at 20%
I don’t know about you, but looking at those requirements, I have several questions.
- Do you earn points on the tax or just the base flight?
- Can you earn points when you are redeeming them?
- Can you redeem more than 100 points at a time if you have them?
- If you’re redeeming points, what amount are you taxed on?
- I also wonder if points are rounded up or down on partial amounts, do you earn partial points? (Apparently the standard for this is to round down)
And that’s just off the top of my head. So you take the questions, and as a team come up with testable examples.
On the assumption that a specific flight costs £50:
- If you pay in cash it will cost £50 + £10 tax and you will earn 50 new points
- If you pay entirely with points it will cost 500 points + £10 tax and you earn 0 new points
- If you pay with 100 points, it will cost 100 points + £40 + £10 tax and you will earn 0 new points
You can now create your scenarios in preparation for developing the feature. Gherkin is quite good for this, as it’s a formal language for examples, but you don’t have to use it. Another thing to note is that scenarios are there to create a shared understanding of a feature, to give an initial definition of done and indicate how to test a feature; they are not contracts, and they can evolve as you gain more knowledge and understanding. But you have to start somewhere. The best thing about writing the scenarios like this, is that it avoids any implementation details (like clicking on buttons and loading pages), which should not form part of the test.
Scenarios are well and good to illustrate how the rules apply; however, you have to make sure you’re all on the same page. Does the word that you’re talking about mean the same thing to everyone else, is it even the right word?
“DDD tackles complexity by focusing the team’s attention on knowledge of the domain” — Eric Evans.
It is important to invest some time in understanding the business. Create a domain dictionary, a ubiquitous language, a shared way of talking about business concepts. It will reduce the cost of translation, so a developer knows that if they refer to an object as a flight, then the business expert will know exactly what that refers to in the context of the business. Use that language in creating your examples.
Combining aspects of these two paradigms gives you Modelling by Example.
The principles of Modelling by Example are:
- The best way to understand the domain model is by discussing examples
- Write scenarios that capture ubiquitous language
- Write scenarios that illustrate real situations
- Directly drive the code from these examples
The domain model isn’t the code, or a diagram. It is the shared understanding the whole team (developers, testers and the business) have of how the system operates. The code, the tests, and the documentation are all representations of the domain model.
Now you may have heard of the testing pyramid (the antithesis of this being an icecream cone or cupcake—bad for the project, but yummy nonetheless). Making sure that your project doesn’t become top heavy with slow tests that break easily is the ideal; everyone hates waiting for the test suite to run only to have a test randomly break because of a network outage or something failing to render.
Testing every scenario through the User Interface (UI) is slow, brittle and it makes you design the domain and the UI at the same time. There are also problems with testing every scenario with the actual infrastructure, for example, testing the business model can result in 1000 tests that find an object by it’s ID (which after one test we can be farily sure it’s going to work), rather than testing interesting edge cases.
What you can do is test the domain model (the middle bit) first using Behat, with fake (in-memory) infrastructure and drive the code directly from the scenarios you’ve written.
Start improving your scenarios by adding realistic details. Interrogate the domain to ensure that you are using the right terms. Find out how the business actually thinks about things and what words they use (see reference above to the domain dictionary). You need to get good at listening. (Actually this is good advice, whatever you are doing).
In the airline example, we started out by using the following:
Given a flight costs £50
By adding realistic examples, we got:
Given a flight from "London" to "Manchester" costs £50
Actively seeking terms from the domain and finding out how the business actually works, this turns into:
Given a flight "XX-100" flies the "LHR" to "MAN" route
And the current listed fare for the "LHR" to "MAN" route is £50
Now, this gives us something we can actually work with. We don’t need to make any assumptions about how the system works, and when we talk about the project to the people involved, everyone will be on the same page. If you don’t feel that you have all the answers, then go back and ask. Your code will be all the better for it, and so will the product.
So now we have workable scenarios, we take those and use Behat to drive the domain model.
Just take it one step at a time. Start with the first line. Create a pending example, then add code just for that example.
Model all your values as Value Objects
This gives you greater control over your data, and allows stricter typing and validation. You can use
@Transform in Behat to convert your strings into the right format, (a new thing I just learned about), which is really useful. Additionally, you end up with neater and more readable test code, which is a definite plus!
Obviously, when you run the first example it’s going to fatal error because you haven’t created any classes, this is okay :) That’s how you do it. You should always start with a “failing” test.
Take a step away from the Behat suite, and start describing the objects you need with PhpSpec (or whatever your preferred method is for unit testing). I really recommend PhpSpec, because it turns your thinking away from “testing” the code to “describing” the functionality; and in my opinion, results in much better (and leaner) code. Only write the minimum amount of code you need to make each spec pass. If you need to add more code, then first add a spec (for example testing valid and invalid input).
Once you’ve described and created your objects, then the first line of your Behat scenario will pass.
Model boundaries with Interfaces
Interfaces are always good, because it means that you are never testing someone else’s code (by which I mean third parties, not other developers on your team!). Plus, you can then create an in-memory version of the interface for testing. It is a separations of concerns to reduce dependancies, and one of the principles behind EBI Architecture (Entity-Boundary-Interactor), which is also known as Hexagonal Architecture.
Once you start describing classes that do stuff with your Value Objects, you need to consider whether using concrete objects (eg
new Thing()) or whether using a double is better? There is an interesting discussion with Ciaran regarding this; but my takeaway from it is that if you think of Value Objects as
final then they “can’t” be doubled; plus it makes the tests easier to read — but also, doubling is mostly used when you have other dependancies, and want tests to be isolated. You have to find the balance that works for you.
By the way, when you are writing your models and methods, think carefully about naming. They should resemble as closely as possible, real world things and actions, and not implementation details.
At this point you should have set up the basic structure of your scenario, but the models will be lacking any functionality until you get to
Then is when you start to add the meat, the logic, and link together in a sensible way what you’ve created so far.
Start to add the extra validation or options you need to make the other scenarios pass, for example, paying with points instead of cash or a partial payment. Use the business logic you’ve already gained to ensure that you’ve covered all the possible scenarios. The cost at this point is negligible, because it’s all done in-memory, and runs independently of any dependancies.
Finally: End-to-End testing.
As you’ve already modelled the domain, and know that all the scenarios are covered within it, the UI testing does not have to be as comprehensive. You don’t need to have 10 tests that click the same button but just return different numbers, because that’s already covered in the middle layer. You can instead cherry pick the appropriate scenarios to test and focus on the interactions and the user experience (UX). Plus the actual UI code is easier to write.
At this point I would also recommend doing some specific end-to-end tests on any APIs you have written for the project, to ensure the infrastructure (that we ignored earlier) works as expected.
Modelling by Example…
- Focuses attention on use cases
- Helps developers understand core business domains
- Encourages layered architecture
- Speeds up test suites
You should be using it when…
- The project is core to your business
- You are likely to support business changes in the future
- You can have conversations with stakeholders
Do not use it when…
- The project is not core to the business
- You are building a prototype or short-term project
- It can be thrown away when the business changes
- You have no access to business experts (but do try and change this)
Some final thoughts. Software development is a continuously evolving ecosystem. No-one gets it right first time, but you learn as you go along and are continuously evolving and improving (even if it doesn’t feel like it!). Pick what works for you. Develop your own process, improve on it, share it with your colleagues and learn from them as well.