If you’ve been following this blog for some time, you know I develop software mostly using test-driven development AKA TDD. But while this mostly means unit tests, it shouldn’t be limited to only unit tests.
At one of my current clients, we use AWS Lambda functions written in TypeScript. These are (usually) relatively small blocks of code that can be invoked by a HTTP call. The function has a very limited scope of what it can be used for, i.e. it isn’t a full-fledged application by itself.
This is where the combination of behavior-driven development and acceptance tests (or end-to-end tests) comes into play.
What is BDD?
In my opinion, BDD is where TDD meets domain-driven design. BDD means that the automated tests of our software must use languages and tools that match those of the business.
This is often translated into a Given-When-Then language. For example, instead of creating a test that is called RetiredGoldCustomerDiscountTest, we could name it GivenARetiredGoldCustomer_WhenTheDiscountIsCalculated_ItShouldEqual10Percent.
But for a really readable syntax, especially if you want it to be readable by the business, our best option is the Gherkin syntax. This allows us to write tests as readable documents. The above example could be something like this:
Given a retired customer with "gold" status When the discount is calculated Then it should be equal to 10 percent
Of course, we need to link this to actual code, which is where something like CucumberJS or SpecFlow comes in. There are Gherkin frameworks for all major languages. But I’m not here to tell you how to use those. They usually have good documentation. Rather, I’d like to propose that acceptance tests might be able to flip over the test pyramid when writing serverless functions.
The Test Pyramid
Remember that the test pyramid tells us to write more tests with few moving parts and less tests with a lot of moving and connected components:
This definitely works with larger applications. But when you start splitting out your application in functions, can your unit tests become end-to-end tests?
When a function is so simple and small, it might not make a lot of sense to write unit tests for individual methods, unless you have some complex ones. Rather, you could just write a test that calls your function and checks the result. The inner workings of the function are no longer relevant.
Of course, you might still want to mock out external dependencies. But when you write a unit test for a class or module, this is effectively an end-to-end test when viewed from the context of that single class or module. When this class or module becomes a serverless function, can’t we just keep the tests that only touch the outer boundaries of said class/module?
I see no direct need for applying the test pyramid religiously here. You could still write unit tests for individual functions and use TDD to drive your design, but you might also end up with an inverted pyramid: mainly tests that call the outer boundary of your function and some tests for individual components inside your function.
Back To Cucumber/Gherkin
What this means for our test suite is that we can leverage the Gherkin language to write most of our tests. Because we’re testing our functions end-to-end, we can write our tests in a language that the business understands. Our tests no longer need to mention the inner workings of our functions. For example, we don’t need to talk about repositories, events, or other technical details.
This leads to great advantages because our Gherkin tests are readable by both technical and non-technical people, provide documentation of both newcomers and seasoned colleagues, and test (almost) the entire function.
Testing Serverless Applications Is Different
It doesn’t seem ideal to simply port our testing conventions from monolithic or even microservice applications over to serverless applications. I believe that we can invert the test pyramid without too much problems, if we at least keep our functions small enough.
But it probably requires some more thought, time and experience to see where this goes. Currently, I’m implementing CucumberJS tests for individual functions with good results.
Of course, the next step up is more challenging than with monolithic applications. Testing the entire system, i.e. the interaction and integration of all the different functions.