Well-written tests significantly reduce the risk of “breaking” the application when adding a new feature or fixing an error. In complex systems consisting of several interconnected components, the most difficult is to test their points of contact.
In this article, I will talk about how we faced the difficulty of writing good tests while developing a component on Go and how we solved this problem using the RSpec library in Ruby on Rails.
Adding Go to the project's technological stack
One of the projects developed by eTeam, where I work, can be divided into: admin area, user account, report generator, and request processing from various services with which we are integrated.
The part responsible for processing requests is most important, so I wanted to make it as reliable and accessible as possible. As part of a monolithic application, she risked getting a bug when changing unrelated areas of code. There was also a risk of dropping processing while loading other components of the application. The number of Ngnix workers on an application is limited, and as the load increases, for example, opening many heavy pages in the admin area, free workers ended and the processing of requests slowed down, or even completely dropped.
')
These risks, as well as the maturity of this system (for months it did not have to make changes in it) made it an ideal candidate for allocation to a separate service.
This separate service was decided to write on Go. He had to share access to the database with the Rails application. Responsibility for possible changes to the table structure remained with Rails. In principle, such a scheme with a common database works well, while there are only two applications. It looked like this:

The service was written and deployed to separate Rails instances. Now, with the Rails deployment, it was possible not to worry that this would affect the processing of requests. The service accepted HTTP requests directly, without Ngnix, used little memory, was in some way minimalistic.
The problem with our unit tests in Go
In the Go application, unit tests were implemented, and all requests to the database were locked. Among other arguments in favor of such a decision was the following: the main Rails application is responsible for the database structure, so the go-application does not “own” the information for creating the test database. The processing of requests for half consisted of business logic and half of the work with the base, and this half was completely locked. Mocks in Go look less “readable” than in Ruby. When adding a new function to read data from the database, it was necessary to add moki for it to the set of fallen tests that worked before. As a result, such unit tests were ineffective and extremely fragile.
Solution Method
To eliminate these shortcomings, it was decided to cover the service with functional tests hosted in a Rails application and test the service on Go as a black box. As a white box, it would still not work, because from ruby, even with all the desire, it would be impossible to intervene in the service, for example, to wet some of its methods to check if it is being called. It also meant that the requests sent by the service being tested are also impossible to lock, so another application is needed to capture and record them. Something like RequestBin, but local. We have already written a similar utility, so we used it.
The following scheme turned out:
- rspec compiles and runs the service on go, passing it a config, which registers access to the test database and some port for receiving HTTP requests, for example, 8082
- also runs a utility to record incoming HTTP requests to it, on port 8083
- write normal tests for RSpec, i.e. we create the necessary data in the database and send a request to localhost: 8082, as if to an external service, for example using HTTParty
- parsim answer; check changes in the database; get the list of recorded requests from “RequestBin” and check them.
Implementation details:
Now how it was implemented. For the purpose of the demonstration, let's call the service being tested: “TheService” and create a wrapper for it:
Just in case, I’ll make a reservation that the Rspec should be configured to autoload the files from the “support” folder:
Dir[Rails.root.join('spec/support/**/*.rb')].each {|f| require f}
“Start” method:
- reads from the separate config the path to the source code for TheService and the information needed to run Since This information may differ from different developers, this config is excluded from Git. The same config contains the settings required by the program being run. These heterogeneous configs are in the same place just so as not to produce unnecessary files.
- compiles and runs the program via “go run {path to main.go} {path to config}”
- interrogating every second, waits until the running program is ready to accept requests
- remembers the process identifier in order not to restart and be able to stop it.
config itself:
#/spec/support/the_service_config.yml server: addr: 127.0.0.1:8082 db: dsn: dbname=project_test sslmode=disable user=postgres password=secret redis: url: redis://127.0.0.1:6379/1 rails: main_go: /home/me/go/src/github.com/company/theservice/main.go recorder_addr: 127.0.0.1:8083 env: PATH: '/home/me/.gvm/gos/go1.10.3/bin' GOROOT: '/home/me/.gvm/gos/go1.10.3' GOPATH: '/home/me/go'
The “stop” method simply stops the process. Newans is that ruby runs the “go run” command that runs the compiled binary in a child process whose ID is unknown. If you simply stop a process started from ruby, the child process does not automatically stop and the port remains busy. Therefore, the stop occurs by Process Group ID:
Now let's prepare a shared_context in which we define variables by default, start TheService if it was not started, and temporarily disable the VCR (from his point of view we communicate to an external service, but for us now it is not quite like that):
and now you can start writing the specs themselves:
TheService can make its HTTP requests to external services. With the help of the config, we redirect to a local utility that writes them. For her, too, there is a wrapper for starting and stopping; it is similar to the “TheServiceControl” class, except that the utility can be simply run, without compilation.
Extra buns
The Go application was written in such a way that all logs and debug information are output to STDOUT. When run in production, this output is sent to a file. And when it starts from Rspec, it is output to the console, which helps a lot with debugging.
If the specs are selectively chased away for which TheService is not needed, then it will not start.
In order not to waste time on development at the start of the service each time the spec is restarted, you can start the service manually in the terminal and not turn it off. If necessary, you can even run it in IDE in debug mode, and then the spec will prepare everything you need, throw a request for service, it will stop and you can debug it without any fuss. This makes TDD approach very convenient.
findings
This scheme has been working for about a year and has never failed. The specs are much more readable than the unit tests on Go, and do not rely on the knowledge of the internal structure of the service. If for some reason we need to rewrite the service in another language, then we will not have to change the specs, except for the wrapper, which simply will have to start the service being tested by another team.