test pyramid

There was a time I argue within my dev team what a unit test is. Many devs write unit tests with a real DB interaction or with other dependecies but they still call it unit tests. I strongly agree that when we want to test interactions, we should NOT mock the DB with 3rd party dependencies like “github.com/DATA-DOG/go-sqlmock”:

When we want to test interactions, we should and always use real DB. Don’t write code like above.

However, here I want to emphasize that unit tests should be free of any dependencies, especially any I/O, and make use of test doubles such as mocks, stubs, spies and fakes.

In unit testing, test doubles are objects that are used to replace real dependencies of the system under test. Test doubles are used to isolate the system under test from its dependencies, making it easier to test the system’s behavior in isolation.

There are several types of test doubles, including:

🦥 Mocks:

Objects that simulate the behavior of a real object, but with pre-defined responses. Mocks are used to verify that the system under test interacts with its dependencies correctly.

Mocking a database connection:

We define a mock database connection that implements the same interface as the real database connection. We then pass the mock database connection to the function being tested instead of the real database connection. This allows us to test the function’s logic without actually connecting to a real database.

func (m *mockDB) Query(query string) ([]string, error) {
    return []string{"result1", "result2"}, nil
}

The above Querymethod in the mockDB struct is used to define a mock implementation of the Query method of a real database connection.

In this example, we are creating a mock database connection to test the GetResults function. The mockDB struct implements the same interface as the real database connection, which includes a Query method.

The Query method of the mockDB struct returns a pre-defined result instead of actually querying a real database. This allows us to test the GetResults function without actually connecting to a real database.

Note: the GetResults function would typically call the Query method of a real database connection to retrieve data from the database.

🦀 Stubs:

Objects that provide pre-defined responses to method calls. Stubs are used to simulate the behavior of a real object, but without the complexity of the real object.

Stubbing a file system:

We define a stub file system that implements the same interface as the real file system. We then pass the stub file system to the function being tested instead of the real file system. This allows us to test the function’s logic without actually reading from a real file system.

Note: mocks are typically used to verify that the system under test interacts with its dependencies correctly, while stubs are typically used to provide simple responses to method calls.

🕷️ Spies:

Objects that record information about method calls made to a real object. Spies are used to verify that the system under test interacts with its dependencies correctly.

Spying on a function call:

Here we define a spy logger that records all log messages that it receives. We then pass the spy logger to the function being tested instead of the real logger. This allows us to test that the function calls the logger with the expected message.

Note: A spy is different from a mock in that it does not provide pre-defined responses to method calls. Instead, a spy records information about the method calls made to a real object, which can be used to verify that the system under test is using its dependencies correctly.

🦦 Fakes:

Objects that provide a simplified implementation of a real object. Fakes are used to simulate the behavior of a real object, but with less complexity.

Using a fake HTTP client:

We use the httptest package to create a fake HTTP response. In this example, we use the Do method of the http.Client interface instead of the Get method. The Do method is more flexible than the Get method, as it allows us to customize the HTTP request in more ways. However, the Get method would also work in this example.

Note: A fake is a type of test double that provides a simplified implementation of a real object. A fake is used to simulate the behavior of a real object, but with less complexity. A fake is typically used when the real object is too complex or too slow to use in unit tests.

In Summary:

The main difference among the above 4 methods is how they provide responses to method calls made to dependencies.

Fakes and stubs can be used when the interaction with the dependency is not important to the test, whereas spies and mocks should be used when the input and responses matter.

🥷🏻 Close to the Pyramid Top: Contract Test

🦧 Top of the Pyramid: E2E Test Example

For fun: check within your dev team, and you might find out that even some seniors do not know what a unit test is.