Go‐through抽象化におけるデータベース相互作用のテスト方法


目次

  • Unit tests
  • Strategies for creating unit tests
  • To use or not to use mocking libraries?
  • Is Golang an OOP language?
  • Interfaces
  • Abstraction
  • The coded solution
  • Conclusion
  • LTVでは、我々のエンジニアリングチームは、我々がそれを書くとき、我々のコードをテストすることに気をつけます.それは私たちのコードをより弾力的にします、意図しない結果により弱い傾向があって、繰り返しコードを展開する自信を与えます.テストの多くのタイプがあります(例えば、単位テスト、回帰テスト、統合テスト、セキュリティテストなど).[1] ), このポストは、単位テストに集中します.

    単体テスト

    A unit test is a low-level test that accesses a small component of our software and follows these steps:

    1. Prepares the “environment” to make sure that the portion of code to be tested is going to be functional when it is executed.
    2. Updates the environment with fixed arguments or updates needed according to the test case.
    3. Runs the component that is being tested.
    4. Compares the results with what is expected.

    When we say “environment”, we are referring to artifacts in the application such as environment variables and arguments to the code under test, rather than external factors such as ports, internet status or system memory.

    Understanding why tests must be isolated is important. There are many variables outside our code that can affect the behavior of the program. When the test runs, having those dependencies could make our tests return different results every time they run. One example of this is when a program interacts with a database, there are many errors that can happen while our program is running: wrong credentials were used, the database just restarted, a query had a bad format, a deadlock occurs, a port is already in use, etc. To avoid some situations (if they are not relevant for our tests), it’s a good idea to abstract the database interaction and use a mock 部品はテストする必要があります.
    私たちは、個々のテストとして、他のテストとは独立して、各テストを書く.これは、テストT 1、T 2、T 3を有することを意味する.「サブテスト」は親としか関係がなく、互いに隔離されなければならない.以下のイメージはこの考えを明確にします.

    あなたが見ることができるこのイメージで:

  • T 1、T 2、T 3の間に分離.

  • T 1−1とT 1−2は、父T 1の状態に関連している.

  • それらの間にt 1−1とt 1−2が隔離される.
  • ユニットテスト作成戦略

    Creating unit tests is not always simple. Keep a few important points in mind while you create your tests:

    • Keep them isolated: Keep tests isolated from databases, the file system, OS services, networking interaction, etc. Because if they fail, the result of our tests will change for reasons unrelated to the code. If we keep these dependencies, we may end up with many different results for the same tests while they are running and the review of all those possibilities may waste time.
    • Use interfaces in your code: By using interfaces, we can interchange objects or even use a custom object (like a mock) that implements the interface with specific behavior.
    • Use independent tests: Keep each test independent to ensure that one test will not change the result of others.
    • Think about all possible cases: Think about the happy path and potential unhappy paths when writing your test cases.

    モッキングライブラリを使用するかどうかは?

    There are different mocking libraries for Golang, and we have done some analysis to understand if using them is beneficial.

    Even if libraries reduce coding time and are versatile tools that allow us to mock or stub, they could add unnecessary and unused code that make the tests difficult to understand. What you could end up with is obscure code that is not easily understood nor maintained.

    Granted, we write more code by not using mocking libraries, but it shouldn’t be an arduous task because our code should be modular, easy to understand and well decoupled. It also depends on the nature of what we are testing.

    A very important aspect of writing our own mocks is that the tests explain the main code by themselves, so they really complement the code.


    ゴングはOOP言語ですか?

    In the following sections there will be more Golang code, and we found meaningful to answer if Golang is an object-oriented programming (OOP) language and as the Golang team explains, the answer is “yes and no”, as you can see on their official web page: https://golang.org/doc/faq#Is_Go_an_object-oriented_language .
    目的のために、それがオブジェクト指向プログラミング言語であることに同意しましょう.

    インターフェース

    An interface is a type or structure that defines the methods (with their signatures) that need to be implemented for any object to fulfill it. This allows us to have objects that implement this interface with different behaviors.

    To clarify this let’s see an example:

    Having an interface “Teacher”.

    type Teacher interface {
        GetMainCourse() string
        SetName(string)
    }
    

    We could have an object that implements this interface, which could be achieved by adding methods that follow the signatures of the methods defined in the interface.

    type CSTeacher struct {
        name       string
        mainCourse string
        salary     float64
    }
    
    func (cst *CSTeacher) GetMainCourse() string {
        return cst.mainCourse
    }
    
    func (cst *CSTeacher) GetSalary() float64 {
        return cst.salary
    }
    
    func (cst *CSTeacher) SetName(name string) {
        cst.name = name
    }
    

    We can add more methods as needed but we should implement the ones defined in the interface at a minimum. Golang is an intelligent language such that it checks if the contracts are being followed in order to define if an interface is being implemented. We don’t need to specify that CSTeacher implements Teacher in the above example. This way we can abstract the solution to our problem using interfaces.


    抽象化

    Before we go deeper into our solution for testing database interaction in Golang through abstraction, we should define what we mean by abstraction.

    Abstraction in programming is a design technique where we avoid the irrelevant details of something and keep the relevant ones exposed to the exterior world.

    The same is available in Object Oriented Programming (“OOP”) where we can expose the methods for a class (as public methods) and hide their implementation (e.g., We usually don’t care about the implementation of a method of a library, we just use it) and also hide methods (as private methods) that are usually helper methods in a library (e.g. Methods like validators that do something specific for the internal use of a library and we don’t need to export them).

    We can take advantage of interfaces that will allow us to use a custom implementation of the exported methods with the needed behavior, or just add new methods to the object that implements the interface.

    Are we talking about abstract database interactions for testing?

    Yes, database libraries export some methods and their implementation is not very important to us. But their behavior could be important, so we define interfaces in our code to use custom implementations of the interfaces and use a custom implementation for our tests.


    符号化解

    In this section we present a code snippet to show how to use interfaces and mocks with the application of abstraction to test how our code interacts with external factors like a database.

    インターフェイスの作成

    First, let’s create the interfaces that will allow us to keep the database library code and implemented mocks separate.

    package mydb
    
    import (
        "context"
    )
    
    // IDB is the interface that defines the methods that must be implemented.
    type IDB interface {
        QueryRowContext(ctx context.Context, query string, args ...interface{}) IRow
    }
    
    // IRow defines the methods that belong to a Row object.
    type IRow interface {
        Scan(dest ...interface{}) error
    }
    

    模倣を作成する

    We can create mocks with different behaviors as needed.

    package mydb_test
    
    import (
        "context"
        "some_path/.../mydb"
    )
    
    type dbMock struct {
        queryRowContextResult *rowMock
    }
    
    // QueryRowContext queries a database.
    func (db *dbMock) QueryRowContext(ctx context.Context, query string, args ...interface{}) mydb.IRow {
        // Here we can do something with the arguments received,
        // like saving the query to validate it later, etc.
        return db.queryRowContextResult
    }
    
    type rowMock struct {
        scanError   error
        valuesInRow []interface{}
    }
    
    // Scan reads a value from a row.
    func (r *rowMock) Scan(dest ...interface{}) error {
        if r.scanError != nil {
            return r.scanError // We can have a specific error.
        }
    
        // Specific and customized scan code goes here, using valuesInRow if you want.
    
        return nil
    }
    

    インターフェイスの使用

    In the code below, the method ValidateStudents receives an interface as a parameter, so we can pass it the real database object or use a custom mock.

    In the tests, we pass our mock objects and in the production code we pass the objects that the database library provides.

    package blog
    
    import (
        "context"
        "some_path/.../mydb"
    )
    
    // ValidateStudents does some validation over the recorded students.
    func ValidateStudents(ctx context.Context, db mydb.IDB) error {
        students := NewStudents()
        err := db.QueryRowContext(ctx, "SELECT ...").Scan(&students)
        if err != nil {
            return err
        }
    
        // Code that validates students would go here.
    
        return nil
    }
    

    結論

    Testing is an important part of any project. Without tests, we are less equipped to recognize failing code before placing into production and our end-user experience could suffer.

    To achieve the isolation in unit testing we can apply abstraction. With abstraction and the use of interfaces, we can have modules decoupled to interchange between a mocked module for testing and another module used for production.


    Interested in working with us? Have a look at our careers page そして、あなたが我々のチームの一部でありたいならば、我々に手を伸ばしてください!

    参考文献

    Softwaretestinghelp.com. n.d. Types Of Software Testing: Different Testing Types With Details. [online] Available at: https://www.softwaretestinghelp.com/types-of-software-testing/ [アクセスは2020年6月24日].