program testing can be used very effectively to show the presence of bugs but never to show their absence.

1 - Overview

Continuous Integration relies heavily on automated testing

Writing tests (i.e. self-testing code) is a requirement for software developers. It is important to know that these tests can be categorized into different types (e.g. Unit Test, Acceptance Test, etc). Unfortunately, the internet contains varying definitions for each of these test types and it can be quite confusing to developers.

In this article you will learn about the different types of tests and how the Fabric-Team differentiates between them.

This article mainly focuses on testing back-end applications, however it can also be applied to front-end apps.

2 - Test Types

broad-stack tests have the advantage of exercising the application with all its parts connected together and thus can find bugs in the interaction between components in the way that component tests cannot. However broad-stack tests also tend to be harder to maintain and slower to run than component tests

Test Type

Description

Other

Unit Test

Component Test

  • the primary purpose of a single Unit Test is to test a single path/edge-case within a single unit of code (hence its name). A unit is often a class in Java but in some cases it may be a single method or a set of classes.
  • a Unit Test should be fast (e.g. ~20 ms per test). Therefore, mock anything that is time consuming external to the unit
  • create just enough Unit Tests such that all paths/edge-cases of the unit is covered. These tests are typically within a single test class where its name follows the pattern: *Test.java (e.g. EmailServiceTest.java will contain all the Unit Tests that tests EmailService.java)
  • Unit Tests make sure that you’re building the thing right
  • https://martinfowler.com/bliki/UnitTest.html
  • https://martinfowler.com/bliki/ComponentTest.html

*Test.java

Integration Test

  • the primary purpose of an Integration Test is to test the functionality of a subsystem. A subsystem is a set of 2 or more contiguous units. In Java this is often a set of classes that work together to produce a result (e.g. when creating a new user, test that the Rest API and business logic works. Or tests that the business logic and DAO layer works)
  • an Integration Test may take longer than 20 ms but when running them as part of the build they should be fast (e.g. ~20 ms per test)
  • an Integration Test may not test every path/edge-case through the subsystem
  • an Integration Test can be used when a Unit Test is unable to test certain conditions
  • an Integration Test should generally mock calls to external systems
  • an Integration Test should include at least one End-to-End Test for each technical case

*IT.java

End-to-End Test

E2E Test

Full-Stack Test

Broad-Stack Test

  • the primary purpose of an E2E Test is to test the system’s workflow from beginning to end (e.g. when creating a new user, make sure the Rest API works and flows through the business logic and down to the DAO and the SQL is executed successfully)
  • an E2E Test is a type of Integration Test where subsystem = entire system
  • what we discussed so far is a Vertical E2E Test. There is also a Horizontal E2E Test. This is best explained with an example where we have customer on an eCommerce website. The checkout workflow starts with adding items to cart, checkout cart, choose shipment option, choose payment option, and finish
  • https://martinfowler.com/bliki/BroadStackTest.html

*IT.java

same as Integration Test

Cross-System Test

  • the primary purpose of Cross-System Test is to verify connectivity to an external system. An external system can be: an application with API endpoints, a database, a message queue, etc
  • a Cross-System Test may also verify the response of the external system (e.g. when fetching the weather from http://sky.net the response should be HTTP 200 and schema similar to {“weather”:“String”, etc})
  • a Cross-System Test is a type of Integration Test that verifies 2 systems at their integration point
  • at least 1 Cross-System Test should be created for each external system
  • a Cross-System Test is typically not part of the build process, as failures does not necessarily mean the deploying application is at fault, it could very much be a problem on the external system side. However, this can be ignored if the external system is highly predictable and fast

*XT.java

Cross-System E2E Test

  • a broader form of E2E Test that tests the workflow across 2 or more systems (e.g. testing OAuth’s M2M Authorization which requires at least 3 systems)
  • a Cross-System E2E Test uses real external systems while a pure E2E Test will mock all calls to external systems
  • a Cross-System E2E Test is typically not part of the build process, as failures does not necessarily mean the deploying application is at fault, it could very much be a problem on the external system side. However, this can be ignored if the external system is highly predictable and fast

?

Acceptance Test

User Acceptance Test

  • the primary purpose of Acceptance Test is to test whether the requirements of specification or contract are met (e.g. user functionality, common use cases in production, etc)
  • an Acceptance Test has no time limit, but when running them as part of the build they should be fast
    • a long running test should not be part of a build (in some cases, you may choose to skip them in DEV builds and only run them in PROD or even UAT)
  • an Acceptance Test may not test every path/edge-case through the code
  • an Acceptance Test is an E2E Test, so it may resemble an Integration Test, but always for the full system
  • an Acceptance Test is driven by user requirements, not code
  • an Acceptance Test may include non-functional requirements testing (e.g. throughput)
  • Acceptance Tests make sure that you’re building the right thing

*AT.java

Performance Test

  • the primary purpose of a Performance Test is to test the throughput/execution-time of a subsystem or unit
  • a Performance Test has no time limit
  • a Performance Test should not be part of any build process (unless you are working on a performance critical application)

*PT.java

Load Testing

  • Load Testing is the process of putting demand on a system and measuring its response

3 - Commonality Between All Test Types

Each Test Should be Predictable not Flaky

When running the same test multiple times it should either: always fail or always pass. A test like that is a Flaky Test. Mock anything that is unpredictable.

Each Test Should Mock External Systems (Generally), except Cross-System Test

When dealing with a datastore, queries are typically mocked. In other cases, an embedded datastore is used in place of the environment’s datastore. This also prevents data corruption. I prefer using embedded databases for all tests, then have a Cross-System Test to assert connection to the actual database (maybe even test basic CRUD operations). If a test uses an actual database, make sure the database state is the same before and after running the test (e.g. if the test adds a row, delete it at the end of the test)

When dealing with an external application, do the same as with a datastore. For example, API calls should be mocked.

Long Running Tests Should Not be Part of the Build Process (Preferably)

We like builds to be to run as fast as possible.

If a long running test must be part of a build, then find ways to shorten it. Perhaps mock long running processes. Or cut down redundancy (e.g. instead of persisting 100 objects to a database, persist just 1 or 2).

Each Test Should have a Perfect Balance Between DAMP & DRY

Readability matters, it doesn’t hurt to have duplication in tests, if it improves readability.

  • DAMP (Descriptive and Meaningful Phrases) increases maintainability by reducing time necessary to read and understand code
  • DRY (Don’t Repeat Yourself) increases maintainability by isolating change (risk) to only parts of the system that must change

I lean towards DRY as reused code means less code to read

Each Test Should test the Function/Behavior Not the Implementation (when possible)

A test should concern itself with the result, not the steps to the result. For example, calling the method square(x) with argument 2 should return 4, we shouldn’t worry about the bit manipulation.

A plus side is that refactoring code doesn’t not require much change to the test. This is because, the implementation changed but not the behavior.

Each Test Should have the form: Given-When-Then (Preferably)

The idea is to break down a scenario/test into 3 sections:

  • given - describes the state of the world before the behavior is tested (usually state setup is done within @Before or the within the actual test method)
  • when - is the behavior that is being tested
  • then - describes the changes expected or the state of the world after the specified behavior

Sometimes, we write this into the name of the test method. For example:

  • public void givenFourUsersInDB_whenCreateNewUser_thenFiveUsersInDB() { … }
Each Test Should have the form: Arrange Act Assert (AAA) (Preferably)

The idea is to group the code within a single test into 3 parts. Which somewhat reflects the Given-When-Then naming format

4 - Other

5 - Not Curated

  • unit testing - testing one unit of code (sometimes a unit is translated to a java class)
    • should constrain the behavior of the unit under test. An unfortunate side effect is that sometimes, tests also constrain the implementation
  • component testing - testing multiple units of code
  • integration testing - testing between 2 units of code at their integration point
    • verifies the communication paths and interactions between units/components to detect interface defects
  • end-to-end testing - testing across several units within a single application
  • test pyramid -
  • assertion free testing - testing without assertions (usually done just to pass code coverage)
  • integration testing - similar to end-to-end testing defined above
    • narrow integration tests - runs external dependencies locally
    • broad integration tests - calls out to real external dependencies
  • code coverage - commonly mistaken as a quality target metric. code coverage only finds untested code and that is all
  • It is important to constantly question the value a unit test provides versus the cost it has in maintenance or the amount it constrains your implementation. By doing this, it is possible to keep the test suite small, focussed and high value
  • Business Facing Test -
  • Specification by Example (SBE) is a collaborative approach to defining requirements and business-oriented functional tests for software products based on capturing and illustrating requirements using realistic examples instead of abstract statements
    • In their original (and common) world view, each time you implement a new UserStory you add one or more tests. This leads you to a simple tracing structure where each story is verified by one or more acceptance tests. But the problem with this approach is that over time the tests grow in complexity with much duplication. In their new world view there is a suite of acceptance tests that describe the application behavior in SpecificationByExample style. Each time they play a new story, they decide how to update this suite to reflect the new behavior. This breaks the simple story-to-test relationship, but results in a much simpler and coherent suite of tests ~ excerpt from https://martinfowler.com/bliki/NashvilleProject.html
  • Test Impact Analysis (TIA)https://martinfowler.com/articles/rise-test-impact-analysis.html#CreationOfSuitesAndTags