In my work I often find that many engineers’
Golang unit tests are written in a problematic way, simply calling the code for output and including various
IO operations, making it impossible to run the unit tests everywhere.
Golang Unit Testing with Mock and Interface
This article explains how to do unit tests properly in
What is unit testing? Features of unit testing
Unit testing is a very important part of quality assurance. A good unit test not only finds problems in time, but also facilitates debugging and improves productivity, so many people think that writing unit tests takes extra time and will reduce productivity, which is the biggest prejudice and misunderstanding of unit testing.
Unit testing will isolate the module corresponding to the test for testing, so we have to remove all relevant external dependencies as much as possible and only unit test the relevant modules.
So some of the unit tests you see in the business code repository that call
HTTP in the
client module are actually irregular, because
HTTP is an external dependency and your unit tests will fail if your target server fails.
In the above example,
DemoClient will call the
http.Get method when doing the
DoHTTPReq method, which contains an external dependency that means he will request the local server, if a new colleague just got your code and there is no local server, then your unit test will fail.
And in the above example, the function
DoHTTPReq is simply an output without any check on the return value. If the internal logic is modified and the return value is modified, although your test can still
pass, your unit test will not work.
From the above example, we can summarize two unit test features:
- No external dependencies, no side effects as much as possible, and the ability to run everywhere
- Checking the output
Another point I would like to mention is that there is actually a ranking of the difficulty of writing unit tests.
UI > Service > Utils
So for writing unit tests, we will give priority to the unit tests for
Utils will not have too many dependencies. Next is the unit test for
Service, because the unit test for
Service mainly depends on the upstream service and database, so we only need to separate the dependencies and then we can test the logic.
So how do you isolate the dependencies? Let’s go to the next section, where we will cover how to separate out dependencies.
What is Mock?
IO dependencies, we can use
Mock to mock the data so that we don’t have to worry about unstable data sources.
So what is
Mock? And how do we
Mock it? Think of a scenario where you and your colleague are working on a collaborative project and your side is progressing faster and is almost done with your development, but your colleague is progressing a little slower and you are still dependent on his services. How do you continue development without
block your progress?
Here you can use
Mock, that is, you and your colleague can work out the data format you need to interact with in advance, and in your test code, you can write a client that can generate the corresponding data format, and the data is false, then you can continue writing your code, and when your colleague finishes his part of the code, you just need to replace the
Mock Clients with the real
Clients and you’re done. That’s what
Similarly, we can use
Mock to mock the data on which the module needs to be tested. The following is an example.
We will have a
MyApplication that also depends on a
YoClient that sends reports. In the above code we will replace the dependent
TestYoClient, so that when the code calls
MyApplication.Yo, it actually executes
TestYoClient.Send, so that we can customize the input and output of the external dependency.
It is also interesting to note that we have replaced
func(string) error in
TestYoClient so that we can control the input and output more flexibly. For different tests, we only need to change the value of
SendFunc. This way we can control the input and output of each test as much as we want.
Another problem you will find at this point is that if you want to successfully inject
MyApplication, the corresponding member variable needs to be either the concrete type
TestYoClient or an interface type that satisfies the
Send() method. But if we use a concrete type, there is no way to replace the real
Client with the
So we can use the interface type to replace it.
What is Interface?
Golang the interface may be different from the interfaces of other languages you’ve come across, in
Golang the interface is a collection of functions. And
Golang interfaces are implicit and do not need to be defined explicitly.
Personally, I agree with this design, because after much practice, I have found that pre-defined abstractions often do not accurately describe the behavior of concrete implementations. So you need to do abstraction afterwards, instead of writing types to meet
interface, you should write interfaces to meet the usage requirements.
Always abstract things when you actually need them, never when you just foresee that you need them.
My personal recommendation is that for several similar processes, we can first organize the code by writing several structures, and then after we find that these structures have similar behaviors, we can abstract an interface to describe these behaviors, which is the most accurate.
At the same time, the number of methods included in the interface in
Golang should be limited, not too many, 1-3 methods is enough. The reason for this is that if your interface contains too many methods, you will have a lot of trouble adding a new code that implements the type, and the code is not easy to maintain. Likewise if your interface has many implementations and many methods, it will be difficult to add one more function to the interface, and you will need to implement those methods in each structure.
Back to the topic, for
YoClient, initially if we don’t use the
TDD approach, then
MyApplication must depend on a formal concrete type, at this point we can write an instance of
TestYoClient type in the test code, extract the common functions to extract the interface, and then go to replace
MyApplication with the interface type.
This will accomplish our goal.
Some other examples
I have also provided an example for reference, mostly from official production code, that masks sensitive information.
This example is a
mock of the external dependency
An example of a unit test:
There are some tips you may not know about testing on my end
golang’s interal and external testing
For a package’s exported methods and variables you can create
test files under the same package to test them, just by changing the package name suffix to
_test. This way you can do
black box testing. The advantage is that you can describe your test from the caller’s point of view, rather than writing your test from an internal point of view. It can also be used as an example to show users how to use it.
Alternatively you can test unexported methods and variables by creating a file with the suffix
_internal_test to identify that you want to test unexported methods and variables.
- Features of
- No external dependencies, no side effects as much as possible, ability to run everywhere
- Need to check the output
- Can be used as an example to show users how to use
Golangcan use interfaces to replace dependencies