We write millions of line codes every day as developers. This codes form programs, interfaces, applications and web sites and they solve business situations and clients use them. But sometimes we encounter bugs on production environment. For overcoming bugs, test methodologies have improvement. One of these methodology is unit-testing.
As Trendyol Tech, we attach great importance to writing unit tests. Apart from this, we set a test coverage target as individual teams. Our code blocks that fail to achieve the test coverage rate receive warnings or errors from the pipeline level. As a result of these, we implement more robust and strong solutions.
Apart from this, there are many applications written in go programming language throughout Trendyol. Our perspective on unit-tests is also valid here and we write unit tests for each line of code.
As customer services team, we write unit tests in our go applications. Our motto is to unit test every single business code. We use standart testing packages as you can see the details from this link.
Writing tests in Go, is provided by using testing.T type. The approach is to pass this type as a parameter to the test function and use functions of that type.
Let’s start the unit test adventure
As a team we started to write unit tests with testing.T type. Also our services has dependencies with other services. For example a controller type has a dependency with service type. So we used stretch/testify for mocking. It required a new type for mocking our service. So we created a type with manually as you can see example of demo that created for this article.
After that we started to write unit tests as you can see below. Engine, controller and mock type were created and GetOrders with on function that getting testify/mock package was mocked. Real function was called for testing scenario. And at the end of functions assertions were here, expected results were waited for success case.
As you realized, we used stretch/assert package for assertions. It helps us for assert something with useful functions like assert.Equal, assert.Null, etc
Is it a tiring and tedious thing to write a unit test?
Business in our team grew day by day and naturally the gateway we wrote with go created new needs. We realized that writing unit test takes more time than writing business codes in tasks. We had times when the work itself was finished but the unit testing was not. At the same time, for the newcomers to our team to understand the unit test functions was difficult as there was no standart in our unit tests both of naming or processes.
Because of these reasons, we set some rules and improved some methodologies by writing unit tests. Let’s see them.
Giving test functions naming as standard and using given, when, then comments
As a result, we first agreed on the name standard. Every member of our team has adopted and implemented this standard.
- For success or default cases; only put function that want to test. Example: TestGetOrders
- For other scenarios; use this standart functionName_WhenSomethingHappened_ReturnsResult Example: TestGetOrders_WhenServiceReturnsError_ReturnsInternalServerError
Function name that execute test, must start with Test prefix in Go so compiler can understand it is a test function and run it.
Thus, we increased the readability and perceived what testing does with a common standard just by looking at the test name. This seems easy but i say that its effect is very useful for team. Developers in our team do not confused by giving test function names.
Another simple but effective method that increases test readability is using Given, When and Then line comments.
This line comments help us to show structure of each test scenario. Thus, it enabled new newcomers who read the test to adapt more easily.
Creating mock types with Mockery package
As aforementioned in first example, when your service has dependency with another interfaces, you should bind them with mocks. This mocks also need another mock type that referenced this interface. In earlier we created this type manually and when new functions had came or function signature had updated, there was a need to change this mock type manually. This process both involved a cost in time and may occur possible development errors.
We used the mockery package to get rid of this cost. Thus, mocking types were created in seconds :)
We used mockery in makefile step as it can be shown. All we did in that, mark the interface we want to mock and write a command like “make mock” in terminal.
It created automatic mock type like this :)
Thus, the process of creating the mock type, which was a boring part of the testing step, was only marking and running a command for us. As we realized that, after using mockery, our speed increased by 25 percent :)
Using Table-Driven Tests
We continued writing tests. But at some points, we realized that, sometimes there may be more than one business condition on the code we write. For this, we copied all test parts except business point. At this point, we may need to handle all conditions in a single test function instead of duplicating test scenarios. As team we used table-driven test methodology. For clarity, i want to show a demo for this methodology.
If you look codes, there is a part that controls statuses and for some statuses, deleting cannot be allowed starting line 14 to 21. We can write unit-tests like this;
I created int array that get statuses that can not be permitted to delete. After that, i ranged over this statuses and per status i run test cases with t.Run method. This also increase readability of tests. When you look test results from ide (As i used Goland Ide), you can see results like this;
This methodology both saves us from writing duplicate tests and allows us to write more understandable test cases. Thus, we have improved both in terms of time and readability.
Using Test Suite
As the business grew, new codes and new unit testing needs emerged. But we realized that while writing the tests, there were initialize lines that repeat themselves. Also, when our controller has new dependency, we should change all old tests for giving new dependency to controller. To prevent these, we can introduce the concept of test suite as team.
Test suites group same test types in logic and generalize initializations of tests. We used package suite of our testify package.
In suite package there are useful functions. For example you can build a suite that runs before all tests. Or you can build a suite after all tests finished. In this respect, it is similar to the annotations (@BeforeEach, @AfterEach) provided by the junit library in java programing language.
In the light of this information, i want to show how we implemented test suite in our tests in below code.
First of all, allow me to explain how we can build test suites. I created a new type named ‘OrderControllerSuite’ and initialize it with suite.Run command. After that it entered SetupTest function and bind dependencies. SetupTest function run before each test in the suite. In our test functions we only write scenarios and control test results.
With this methodology, we did not need to duplicate initialization in every test scenario anymore. So adding a new case become very easy for us, only adding scenario. Also when new dependency has come, only we added it to related struct, not change old scenarios. In the analyzes we made, we saw that this methodology saves us 20% of time.
Summary
As the customer services team in Trendyol, our motto is to integrate unit-test into the business codes we write. In this context, the unit test writing process can be time-consuming in some aspects. At the same time, the unit tests we write may have problems in terms of readability. In this article you see a number of tricks we have solved these problems. In this way, we have achieved much more effective and highly readable tests.
Apart from that, I prepared a github repo that contains demos of what i explained in the article. It is a good way for those who want to deep dive on the subject.
Ready to take your career to the next level? Join our dynamic team and make a difference at Trendyol.