📜 ⬆️ ⬇️

And let the tests themselves and support

Today I want to talk about an unusual approach to writing tests, to which I somehow unobtrusively came to work on several projects of different sizes, and which for some reason I did not meet in a pure form with others, although, in general, lies on the surface. Recently, I began to write some code on Go, and as soon as there was a question about writing tests, I again remembered this approach.

How do tests usually look like?


Very schematically, each unit test usually consists of the following steps:

  1. Initialization of input data;
  2. Run business logic and get results;
  3. Comparison of the result with the standard.

Input and output are often in the code itself; when code changes introduce expected changes to the output, the reference results have to be edited manually. In some cases, when the data for the test are voluminous, they are put into separate files, but the support for the reference data, as well as the comparison logic, remains on the developer’s shoulders.
')

But all this can be unified!


Imagine that in the body of your unit tests there is generally no comparison of the results obtained with the standard. Imagine that the tests themselves can create reference data for you. Imagine that all the input and output data is in a structured format, and the test code becomes more compact, uniform and readable. Submitted?

Meet the agenda- tests


I called this approach “testing” because I love abbreviations, and the agenda is, in fact, a utogen erated da da . What is its essence?

  1. Input and output tests are stored in files (JSON or something else - it does not matter).
  2. The test can work in two modes:

    • Initialization mode: the test calculates the output data and saves this data into a master file;

    • Test mode: the test calculates the output data, reads previously saved reference data and compares it; the data is different - the test failed.

  3. All auxiliary code of the type of reading, writing and comparing data is put into the auxiliary library / function / class, leaving only the essence of the individual tests.

And this is all? .. And this is all! Let's take a look at how this works with the example of Go, for which I have published a small library , and which can easily be ported to any other language.

To begin, create a file of "business logic": the code that we are going to test:

Example.go file
package example import "errors" type Movie struct { TotalTime int `json:"total_time"` CurrentTime int `json:"current_time"` IsPlaying bool `json:"is_playing"` } func (m *Movie) Rewind() { m.CurrentTime = 0 } func (m *Movie) Play() error { if m.IsPlaying { return errors.New("Movie is already playing") } m.IsPlaying = true return nil } 

Now create a test:

File example_test.go
 package example import ( "encoding/json" "testing" "github.com/iafan/agenda" ) func TestMovie(t *testing.T) { agenda.Run(t, ".", func(path string, data []byte) ([]byte, error) { type MovieTestResult struct { M *Movie `json:"movie"` Err interface{} `json:"play_error"` } in := make([]*Movie, 0) //  data       , //      if err := json.Unmarshal(data, &in); err != nil { return nil, err } out := make([]*MovieTestResult, len(in)) for i, m := range in { // , "-"  //  Rewind()    m.Rewind() // Play()  nil   err := m.Play() //   ""  // 1)      Movie // 2)         out[i] = &MovieTestResult{m, agenda.SerializableError(err)} } //        //         return json.MarshalIndent(out, "", "\t") }) } 

All the magic of the agenda-test here in the line:

 agenda.Run(t, ".", func(...){...}} 

Which will take all the test files in the current directory (by default, these are files with the .json extension), and for each, it will start the function passed as a parameter.

Now create a file with test data:

File test_data.json
 [ {"total_time":100,"current_time":0,"is_playing":false}, {"total_time":150,"current_time":35,"is_playing":true}, {"total_time":95,"current_time":4,"is_playing":true}, {"total_time":125,"current_time":110,"is_playing":false} ] 

You can run the test in initialization mode:
 $ go test -args init 

At the same time, a file with reference data will be created next to the input file:

File test_data.json.result
 [ { "movie": { "total_time": 100, "current_time": 0, "is_playing": true }, "play_error": null }, { "movie": { "total_time": 150, "current_time": 0, "is_playing": true }, "play_error": "Movie is already playing" }, { "movie": { "total_time": 95, "current_time": 0, "is_playing": true }, "play_error": "Movie is already playing" }, { "movie": { "total_time": 125, "current_time": 0, "is_playing": true }, "play_error": null } ] 

This file should be analyzed and ensure that the output meets expectations. If all is well, such a generated file, along with the test data, is committed to the repository.

Now you can run the test in normal mode:

 $ go test 

The test, of course, must pass without errors.

Now, when you make changes to the code in the course of the project’s life, you will use two scenarios for working with such tests:


The separation of code and test data has both advantages and disadvantages:

The disadvantages include a greater number of files that will be present in commits. For simple unit tests with simple data of limited volume, tabular tests are more suitable.

There are much more advantages : better readability of tests (both code and data), especially in the case of complex structures of the tested data, less chance to miss something when checking the results, as well as the possibility of replenishment and verification of test data by testers without having to recompile the code.

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


All Articles