(previously published on DZone)
In my previous post, I discussed the difference between tests that target code versus those that target an API. A subset of the second category are automated tests for a web/mobile interface that mimic user behavior and validate the rendered responses, using Cucumber/Selenium, Cypress, or any other stack. These are typically written and executed as end-to-end tests, in that they require a production-like setup of the backend; but that needn’t be the case. GUI tests can turn into true component tests if they target the browser, but with a fully mocked backend. In a complex microservices architecture, it makes good sense to do so. In this article, I will highlight the motivation for writing those tests, and in a follow-up, I will give tips and examples on how to do so with the Karate framework. Feel free to dig into its excellent document if you can’t wait.
Separation of Concerns Between Frontend and Backend
Traditionally, in the world of JSP and JSF, the backend of web applications would contain mostly display and rendering logic, on top of handling all business rules. With so much server-side rendering, you had no choice but to spin up the actual backend code if you wanted to automate a user scenario in the browser. Without the backend, nothing much would work.
Modern JavaScript/TypeScript frameworks like Angular and React have turned the browser into a mature programming platform and frontend development into a legitimate career. The frontend server provides static assets (HTML/CSS/JS) which are executed in the browser and make occasional HTTP requests (usually REST through JSON) to the backend, which is no longer concerned with rendering decisions, but rather with keeping state, delegating to other services and making general decisions.
If the frontend contains business logic of its own, these merit detailed unit tests. However, complicated and crucial logic is not typically written in JavaScript frontend code, so there may not be that much unit testing to do. While you can test GUI control interactions in unit tests, I have never been a fan of it. I find running a complete user scenario in a browser-based test is less cumbersome and verbose. You can’t beat a real browser environment for making sure the right network calls are made. Running such tests involves serving the (compiled) assets to execute in a live browser, but you don’t need the actual backend code to handle the REST interactions. They can all be mocked. Such an environment makes for a true component test.
To recap from the previous post: a component test targets a single, self-contained executable with all external dependencies replaced by test doubles. When you run your GUI tests against a Node server alongside a mocked instance of the backend, you tick the required boxes. It’s a true component test since you have effectively taken the backend out of the equation.
Setting Up a Mock Server
For this, you need to set up a server that replies to the same calls as the real backend and returns preconfigured responses. Setting it up requires some work, but (parts of) the process can be automated. Karatelabs studio and Wiremock Studio can generate it from an openapi spec file, but it really isn’t that hard to write it by hand. Also, consider that preparing end-to-end tests are not cheap, and are much more expensive to run, both in terms of time and resources. Any non-trivial backend probably won’t run on your local machine, not even containerized. There are many mock server solutions to choose from.
Besides the benefit of running completely localized (think speed), another advantage of a mock server is its flexibility. You can provide different responses to the same URL depending on its input, either in the request body or query parameters. This makes it easy to simulate happy/unhappy scenarios, and even error (500) responses or timeouts. In a complicated business transaction, there may be more journeys toward successful completion, with just as many dead ends. Whenever the GUI takes a different route, depending on some return value from the server, you can write an appropriate scenario for it. Consider the following in Karate syntax:
Feature: Create and update a new product
Scenario: methodIs('post') && pathMatches('/api/v1/order') && request.name == 'Al Capone'
* def response = {'success' : false, 'message': 'Customer is blacklisted'}
* def responseStatus = 401
Scenario: methodIs('post') && pathMatches('/api/v1/order') && request.name == 'Mayhem'
* def responseStatus = 500
* def response = 'SERVER_ERROR'
Scenario: methodIs('post') && pathMatches('/api/v1/order')
* def response = {'success' : false, 'message': 'Order created'}
Requests are matched in order, meaning those with a specific value in the name property of the request payload get precedence over the catch-all success scenario. For any backend interaction, there are usually at least three outcomes worth simulating: a successful response, an expected rejection, and a non-recoverable, unexpected server error, like a timeout or HTTP 500. All are very easy to set up (see previous examples). The happy flow describes a possible sequence of steps towards the successful completion of a business transaction of the service. On the road to this destination, there are turns where you can hit a dead end. These termination points are expected and should be handled appropriately by the code. In addition to that, there are irregular responses: server timeouts, empty responses, or any kind of HTTP code outside the OK range where you would expect some error page. Such simulation is much harder to set up in a production-like system.
It’s Not Your Decision, So Don’t Test It
There’s an important reason why old-style end-to-end tests are often more expensive than needed: you tend to test paths that are not relevant to the frontend logic. Each of these adds to the total test suite run. Consider a web application for your tax return. The user journey in this non-trivial app consists of submitting a series of questionnaires, their content customized depending on what you answered in previous steps. There is likely some logic on the frontend to manage the turns in that user journey, but the number-crunching over your sources of income and deductibles surely happens on the backend. You don’t need a GUI test to validate the correctness of those calculations. With a mock backend that would be entirely pointless. You set it up to tell the frontend that the final amount to pay is 12600 Euros. You can test that this amount is properly displayed, but there’s no testing its correctness. All the decisions are made (and hopefully tested) elsewhere, so we can treat it as a hardcoded test fixture.
Scenario: methodIs('post') && pathMatches('/api/v1/confirm-return')
* def response = {'success' : true, 'message': 'Tax return received', 'amount': 12600}
Component tests like the example above are fast because they bypass server logic, but also because they cut down the number of tests without degrading quality. You need fewer of them to reach the same coverage. Consider the final calculation of the tax amount payable, which is based on all the data sent to the backend in the previous steps. It’s relevant for the frontend to test that the correct figures are sent, and the responses appropriately handled, but the final figure received should be treated as a given, since the backend logic is out of scope.
Stay tuned for some practical Karate examples and tips in the follow-up to this article!