Writing Code that tests! utilizing interfaces to write better code in Golang
Coming from a pythonic/rubyish background, I have been struggling really hard to write tests in Go, things were much easier back in my dynamic lang but here doing the simplest isolated test requires numerous hacks and changes to my code!
But then I thought there is surely a better way to do this, I’m definitely doing something wrong, or writing my code in a way that makes it test resistant, maybe there is a better way to write code that is both testable and at the same time elegantly designed.
Testing By Example
Let’s take a real world example, let’s say we want to test this piece of code I wrote in a previous article:
This code essentially what it does is define two functions:
IntializeDBSessionstarts a new Database Connection to Cassandra Database Nodes (A DB Engine like Mysql ,etc)
CreateRequestStatsa batch insert function that takes an array of request objects and writes them in bulk to the request table.
We want to write tests for this code, we want to be able to test the functionality without involving Cassandra, as it won’t mostly be available in the test environment plus it’s irrelevant to test cassandra itself as part of our unit test.
We need somehow to intercept the the data being sent to Cassandra and instead of writing it to the database for real, we will just write it in memory and return the operation as successful.
Sadly doing that in the code’s current state is kind of messy since:
- There is not clear separation between the Database Client(Cassandra package) and our code.
- the client is a private member that cannot be changed by outside effects, only initialized.
This pattern of problems will be the same with any third party we use, call it
elastic_search and many more, so how can we deal with this?
Step back, Refactor
Actually we could have done a better job designing this service, we should in general make a clear separation between any third party and our code, that separation could be done using interfaces.
What we can do here instead of directly giving the ownership of cassandra DB client to the dal package, we could instead write an interface or a contract between DB client and our dal package:
We defined our contract between the DB client and the dal that any object who owns the DB client should provide at least two functions, a function to start a new connection to the DB and another one to do bulk inserts into the db.
This is actually pretty cool, now the dal has no idea what kind of DB engine we are using! as long as the DB client wrapper satisfies the two functions of the interface, everything will work the same!
Let’s implement this interface for Cassandra
As you can see our new interface implementation hides all details of cassandra behind the two functions specified by the interface, also we have a private member for the DB current connection, we added that and our interface won’t mind as long as the interface required functions is implemented.
Now let’s use our new interface implementation in the dal
thanks to the interface, Much bloated code have been removed from the dal, making it focus on its job without worrying about the messy details of every DB engine.
We can pass in the initialization any implementation to the interface and it will work, if none is provided, we fallback to cassandra interface.
Back to Testing
Enough Coding let’s write some tests, remember our first thoughts of a good isolated test?
Yes as you can see below we don’t want to test the DB engine with us, but now as you may already have thought, we have the right piece that replaces the fake dal handler 😉
We can now implement the DB interface in our test and pass it to the dal object, and we will be done!
Now we have our fake DB wrapper, let’s write a test using it to see it in action
- we create a new object for our mocked DB client
- initialized it with the expected results
- passed it to the dal in initialization
- and then call
CreateRequestStats, which will call our mocked interface implementation under the hood and pass to it the queries, there we can verify that the queries are constructed as we expected them to be.
We can write much more advanced tests as we finally got rid of the real DB instance, and we did not have to change our code, we just designed it in a better way that makes resilient to changes and much more solid.
You can check out the full source code for the tests/project here
an open source/in-house application performance management server that monitors your services and allows you to perform…
You can try to practice those concepts yourself by refactoring
elasticsearchmanager using the same approach we used and then write tests for it.