There is a common testing pyramid that many organizations use regarding end-to-end, integration, and unit tests. Analogous to this pyramid, acceptance tests that are developed by the triad (customer unit, developer unit, and tester unit) can be applied at various levels. This blog entry relooks at the context diagram shown in this article, but from the standpoint of testing.
The testing pyramid often looks like the following diagram. The percentages show what should be the percentage of the number of tests at the various levels of tests. There is often some disagreement as to what constitutes integration tests. I’m not going to go into that, but simply use this pyramid as the take-off point for the acceptance test analogy.
Acceptance tests are from the external view of the system. They are implementation independent. Here’s an example diagram for a ticket ordering system. A ticket customer places an order for a ticket for an event. The event has been set up by an event maintainer with the date, time, location, and seats. When the order is placed, a credit card charge is made to an external credit card processor. Then the ticket is the emailed to the customer.
Acceptance tests are written using this overall context. A test might look like:
- Given: Event has been established
- When: Ticket customer places order for seats for event
- Then: Tickets are delivered to customer via email and a credit card charge is processed.
An application’s behavior is dependent on the inputs to it. The entire behavior can be tested through its inputs and outputs. You don’t need any internal tests. However, a number of issues with just using external tests include:
- They can be fragile, since they are dependent on the user interface.
- They can be slow, since business rules can only be tested through the user interface.
- An acceptance test failure only shows that the application does not implement the behavior properly.
Acceptance Test Pyramid
For the above three reasons, you can break down acceptance tests into mulitple levels, as shown in the following diagram. At the top are acceptance tests that are in a production environment. This is usually a pre-production infrastructure that duplicates the production environment. All external entities are real entities. It is more difficult to implement automated testing at rhia level, since external entities usually return non-constant data.
At the next level are automated acceptance tests with test doubles for external entities that return test-specified data. Beneath that are component tests that are derived from acceptance tests. And finally are acceptance tests for business rules and business specified computations. In parallel with that layer, but not shown on this diagram) are the technical tests (“unit test”). These are for modules that implement the components (the ones at the bottom of the testing pyramid that started this article). Let’s take a look at the bottom three levels in a little more detail.
Acceptance tests for business rules and computations
Almost every application has lots of business rules and computations. From informal polls, I’d estimate that between 50 and 70% of the code in a typical application are business rules and computations. These are specified by the customer unit and implemented by modules created by the developer unit. Often the modules are tested with technical testing frameworks, such as JUnit. However to increase collaboration, the tests should be written in a customer-readable format, such as Cucumber or Fit. In our example, a test for a business rule might include something like:
| Seat Cost | Count | Total | Notes | 15.00 | 2 | 30.00 | No discount for 1 to 19 seats | 15.00 | 20 | 270.00 | 10% discount for 20 or more seats | 15.00 | 200 | 2400.00| 20% discount for 200 or more seats
Properly designed tests for all variations in business rules can run fast. They do not involve dependencies on other parts of the applications.
Acceptance tests with test doubles
On the third level up are tests that use test doubles for external dependencies. In the example application, you’d have test doubles for the email server and the credit card processor. These test doubles can be used in both manual tests of the user interface and automated tests. They ensure that the data that should appear on the user interface is known. User interface tests check that the data appears in the right place and that the interface is usable. They do not need to check for business rule correctness, as that is taken care of by the business rule tests.
Here’s the context diagram for the application. In this example, test doubles are created for the email server and the credit card processor.
An example of a test at this level is:
Given Event has been established | Event Name | ID | Date | | Beach Boys | 11 | 1/1/2017 | And Seats have been established | Event ID | Available Seats | Cost | | 11 | A1, A2, A3 | $15.00 | When Ticket customer selects: | Event Name: | Beach Boys | | Seats: | A1, A2 | Then Credit Card is charged | Notation | Amount | | Beach Boys | $30.00 | And Tickets sent for | Event Name | Date | Seats | | Beach Boys | 1/1/2017 | A1, A2| And inventory changes to | Event ID | Available Seats | Cost | | 11 | A3 | $15.00|
Component tests derived from acceptance tests
Trying to test a process flow through the user interface can be fragile. Instead, the acceptance test that applied to the entire application can generate tests for components. As necessary, test doubles for the other components can be created. In this example, the previous test could be applied to the mid-tier component. The same test doubles for email server and credit card processor can be used in these tests. The test is driven by calling the same interface that the user interface component would call for the same operation.
If desired, the database component could have a test double as well, making this a test for just the midtier component.
Acceptance tests are from the external view of the system. However, they can be used as component or unit level tests. Doing so will make the tests faster and more maintainable.