📜 ⬆️ ⬇️

Testing Services API and RSpec

image

Sometimes it is necessary to write a small API service, often in the form of a prototype. And often this prototype then remains in its originally written form following the principle “it works — don't touch it”. Rewriting even a relatively small service is fraught with the possibility of introducing an error or accidentally slightly changing behavior that is not immediately apparent. Here comes the black box testing (functional testing). Writing tests is an important part of the development process, and the time spent writing tests can be much more than the implementation of the tested functionality. I propose to consider the testing method when the code being tested (service) and auto tests are written in different programming languages. This approach allows you to write tests without depending on the originally selected technology, which makes it easy enough to “throw out” the prototype and rewrite the required functionality on other technologies. Plus, this is a demonstration that tests do not have to be written in the same language as the service being tested.

For example, take the following task. Write an http API service with the following methods:

  1. GET / ping - the service should always respond with the code 200 and the text "OK".
  2. GET / movies - service gives a list of movies, which in turn receives from a third-party service. Supports filtering through the query parameter parameter; if the parameter is not specified, it uses the default value.

We will need:


Attention: in this text any details on the installation, configuration and use of the considered tools are deliberately omitted.
')
Rspec as a framework for testing is chosen as the ruby ​​language syntax allows you to write fairly concise tests with a minimum of utilitarian code. MockServer is a very powerful tool for emulating the responses of third-party services, the main feature is that it can run as an independent http API service. If you use another stack of technologies, you will almost certainly be able to find the most convenient counterparts for you. These tools are taken solely for the sake of example.

I skip the steps for installing and configuring ruby, java and golang. Let's start with rspec. For convenience, it is desirable to install the bundler. The list of used gems will be as follows:

gem "rspec" gem "rest-client" gem "mockserver-client" 

Mockserver has a fairly convenient REST API and clients for Java and JavaScript. We will use ruby ​​client, at the moment it is clearly not supported, but the basic functionality is available. We generate application skeleton through the command
  rspec --init 


Then create the file /spec/api_spec.rb:

 # /spec/api_spec.rb require 'spec_helper' require 'rest-client' require 'mockserver-client' RSpec.describe "ApiServer" do let(:api_server_host) { "http://#{ENV.fetch("API_SERVICE_ADDR", '127.0.0.1:8000')}" } end 

Let's write a test for the / ping method (let's put this code section inside the RSpec.describe “ApiServer” block)

 describe "GET /ping" do before { @response = RestClient.get "#{api_server_host}/ping" } it do expect(@response.code).to eql 200 expect(@response.body).to eql 'OK' end end 

If you now run the test (via the rspec command), then it is predictably falling with an error. We write the implementation of the method.

 package main import (    "net/http"    "github.com/labstack/echo" ) func main() {    e := echo.New()    e.GET("/ping", ping)    e.Start(":8000") } func ping(c echo.Context) error {    return c.String(http.StatusOK, "OK") } 

We compile and run our API service (for example, through a go run). To simplify the code, we will start the service and tests manually. First we start the API service, then rspec. This time the test should pass successfully. Thus, we obtained the simplest non-dependent test, with which you can test the implementation of this API in any language or server.

Let's complicate the example and add the second method - / movies. Add the test code.

GET / movies
 describe "GET /movies" do let(:params) { {} } before { @response = RestClient.get "#{api_server_host}/movies", {params: params} } context '?rating=X' do let(:params) { {rating: 90} } let(:query_string_parameters) { [parameter('rating', '90')] } let(:movies_resp_body) { File.read('spec/fixtures/movies_90.json') } let(:resp_body) { movies_resp_body } include_examples 'response_ok' end describe 'set default filter' do let(:query_string_parameters) { [parameter('rating', '70')] } let(:movies_resp_body) { File.read('spec/fixtures/movies.json') } let(:resp_body) { movies_resp_body } include_examples 'response_ok' end end 


According to the condition of the problem, the list of films must be obtained from a third-party IPA, to emulate a response from an external API, use the mock server. To do this, we give it the response body and the condition under which it will respond to them. You can do this as follows:

setup mock
 include MockServer include MockServer::Model::DSL def create_mock_client MockServer::MockServerClient.new(ENV.fetch("MOCK_SERVER_HOST", 'localhost'), ENV.fetch("MOCK_SERVER_PORT", 1080)) end let(:query_string_parameters) { [] } let(:movies_resp_body) { '[]' } before :each do @movies_server = create_mock_client @movies_server.reset @exp = expectation do |exp| exp.request do |request| request.method = 'GET' request.path = '/movies' request.headers << header('Accept', 'application/json') request.query_string_parameters = query_string_parameters end exp.response do |response| response.status_code = 200 response.headers << header('Content-Type', 'application/json; charset=utf-8') response.body = body(movies_resp_body) end end @movies_server.register(@exp) end 


And the implementation of the handler in the IPA service:

movies handler
 func movies(c echo.Context) error { rating := c.QueryParam("rating") if rating == "" { rating = "70" } client := &http.Client{} req, _ := http.NewRequest("GET", "http://localhost:1080/movies", nil) req.Header.Add("Accept", `application/json`) q := req.URL.Query() q.Add("rating", rating) req.URL.RawQuery = q.Encode() if resp, err := client.Do(req); err != nil { panic(err) } else { return c.Stream(http.StatusOK, "application/json", resp.Body) } } 


To run the tests, it is now necessary to start three processes: the service being checked, the mock server and rspec.

 go run main.go java -jar mockserver-netty-5.3.0-jar-with-dependencies.jar -serverPort 1080 rspec 

Automation of this process is a separate task.

It is worth paying attention to the final size of the service code and tests for it. Coverage with a minimum service for 30 lines requires almost three times as many lines of code in tests, with a bulk code for installing mocks, but without taking into account the automation of the launch and fixtures of answers. On the one hand, this raises the question of rationality of testing, on the other hand, this ratio is generally standard and shows that good tests are at least half the work. And their independence from the original technology chosen can be a big plus. However, it is easy to see that it is thus extremely difficult to test the state of the database. One of the possible solutions to this problem is to add a private API for changing the state of the database or creating database snapshots (fixtures) for different situations.

Gist with listing

Discussion, pros, cons and criticism - wait in the comments

Source: https://habr.com/ru/post/350382/


All Articles