Mocking database logic for unit tests in Go at FlightAware

Mocking database logic for unit tests in Go at FlightAware

Lee Obuli is a software engineer on the AeroAPI team who specializes in driving testing efforts with Go at FlightAware.

Go is one of FlightAware’s four core programming languages. It stands out because of its relatively easy learning curve and great performance. Since FlightAware needs a performant solution for delivering massive amounts of data, Go is a great choice. Speed is not the only requirement for our code; it should also behave correctly and reliably. To deliver high-quality code at FlightAware, one essential technique we use is unit testing. If you are interested in learning more about the principles of unit testing, this Codefresh article is a great resource. This blog will focus on a mocking technique for effective unit testing in Go. 

FlightAware relies on PostgreSQL databases to deliver the data that we use on the website and in our APIs. Testing the interactions between Go code a database introduces an external dependency. This makes it challenging to test the code’s behavior without a backing database. Mocking database interactions allows developers to create simulated data objects that mimic the behavior of real databases. This provides control of data inputs and outputs to test the code, without having to instantiate an actual database.

Code

Here is an example of Go code that executes a simple query to get flight data by passing in a flight ID. The function should be tested to ensure its correctness.

The code above starts with defining a Flight type, which represents the object returned by the database. Next, it defines a PostgreSQL client type (PostgresSQLClient) which contains a connection field represented by an interface. This interface must implement the methods that will be used later. An interface is like a blueprint that defines a set of method signatures. It outlines what methods a type must have, without providing the actual implementations. Interfaces enable the use of dependency injection since there may be multiple implementations for the same interface. After the example Go code creates a PostgreSQL client, it calls the method GetFlightByID. For those wondering, c *PostgresSQLClient is the receiver of the method, indicating that the GetFlightByID method is associated with an instance of the PostgresSQLClient.

The example uses Go's type methods to implement the actual work of the application — to fetch a flight by its ID. As a method, it can use the private conn instance, which can be either a real PostgreSQL connection or a mock one. This technique works great for dependency injection to easily switch the implementation of dependencies. When testing, a mock that implements the same interface as a real database controls the interactions and the test verifies the results. With this decoupling, the actual database can be swapped out (e.g., using pgx in production and pgxmock in tests) without affecting the rest of the codebase.

Writing a simple unit test

The next section of code will test the example. I prefer to use pgxmock since it provides a convenient API for setting up and verifying results on database queries. Building the logic to manually mock every database call is cumbersome, error-prone, and time-consuming. While manual mocking may be feasible for simple cases or small projects, a dedicated mocking library that repeatedly mocks a dependency can be used as a project grows in complexity. This helps with writing more maintainable, reliable, and expressive tests for database interactions in an application.

Here is the test for the GetFlightByID function above:

Notice the test verifies a small function that fetches a flight. It ensures that the result values are correctly returned and that all database expectations are met (for example, there are no additional queries that the test didn’t account for).

Explanation of the pgxmock library

Let’s look at how pgxmock helps with mocking the database calls:

  • NewRows creates mocked SQL query rows from a Go string slice. A slice in Go can be considered an array that can increase or decrease in size. Notice in the example columns are passed in by string slice into the NewRows function.
  • AddRow adds rows to the result set of the expected query. The mock data to return should match the number of columns defined in NewRows.
  • ExpectQuery defines the expected SQL query by using query string matching.
  • WithArgs will match given expected args to actual database query arguments and if at least one argument does not match, it will return an error.
  • WillReturnRows specifies the set of resulting rows that the triggered query will return.
  • Then, after executing the function that uses the mock database, ExpectationsWereMet ensures that only the queries expected by the mock were executed during the test.

Conclusion

Mocking downstream functions saves developers time by avoiding the complexity of managing the lifecycle of the mocked objects. The developer can focus on what matters most, which is correctly functioning code. Writing unit tests in Go is not only a best practice, but a fundamental aspect of building reliable and maintainable software. There are several Go mocking packages such as testify, gomock, and mockery. A future blog will focus on utilizing mockery to generate the mocks for the code. These tests (including the example in this blog post) already use the testify package, so incorporating the mockery package will be more straightforward. Stay tuned!

Lee Obuli

Lee Obuli

Lee Obuli is a software engineer on the AeroAPI team who specializes in driving testing efforts with Go at FlightAware.

Show Comments
Back to home