더 나은 통합 테스트를 위한 Go 패키지: github.com/ory/dockertest

18381 단어 godockertesting
데이터 저장소와 상호 작용하는 코드에 대한 테스트를 작성할 때 일반적으로 딜레마에 직면합니다.
  • 데이터 저장소에 대한 호출을 조롱해야 합니까? 또는
  • 실제 데이터 저장소를 사용하여 통합 테스트를 작성해야 합니까?

  • To be clear, when I say interacting with datastores I mean where we are actually implementing the concrete repository talking directly to the datastore, not the application service type using that said repository.



    옵션을 살펴보겠습니다.

    Please refer to the full example code for more details.



    데이터 저장소 호출 모의



    사용 중인 데이터 저장소에 따라 테스트를 작성하는 다양한 방법이 있습니다. 예를 들어 database/sql를 사용하게 된 데이터베이스 호출을 테스트하는 경우 github.com/DATA-DOG/go-sqlmock과 같은 패키지를 가져올 수 있습니다.

    모의 호출에 사용되는 사실상의 패키지가 없는 경우 코드에서 사용하는 구체적인 호출을 정의하는 고유한 인터페이스 유형을 정의할 수 있습니다(예: memcached calls github.com/bradfitz/gomemcache ). 사용 중이면 다음과 같이 작동할 수 있습니다.

    // MemcacheClient defines the methods required by our Memcached implementation.
    type MemcacheClient interface {
        Get(string) (*memcache.Item, error)
        Set(*memcache.Item) error
    }
    
    // AdapterMemcached uses a mocked client for testing.
    type AdapterMemcached struct {
        client MemcacheClient
    }
    


    여기서 AdapterMemcached는 실제memcache.Client와 필요한 메소드를 조롱하는 유형을 수신하여 다음과 같은 테스트를 성공적으로 작성할 수 있습니다.

    func TestAdapterMemcached(t *testing.T) {
        // TODO: Test sad/unhappy-paths cases
    
        mock := mockingtesting.FakeMemcacheClient{}
        mock.GetReturns(&memcache.Item{
            Value: func() []byte {
                var b bytes.Buffer
                _ = gob.NewEncoder(&b).Encode("value")
                return b.Bytes()
            }(),
        }, nil)
    
        c := mocking.NewAdapterMemcached(&mock)
    
        if err := c.Set("key", "value"); err != nil {
            t.Fatalf("expected no error, got %s", err)
        }
    
        value, err := c.Get("key")
        if err != nil {
            t.Fatalf("expected no error, got %s", err)
        }
    
        if value != "value" {
            t.Fatalf("expected `value`, got %s", value)
        }
    }
    


    통합 테스트 작성



    데이터 저장소 호출을 모의하는 것은 확실히 테스트 목적으로 작동하므로 비즈니스 로직에 더 집중할 수 있지만 코드가 실제 환경에 배포될 때까지 통합 테스트가 부족하다는 단점이 있습니다.

    Docker 이전에는 데이터 저장소를 조롱하는 것이 이상적인 솔루션이었지만 요즘에는 로컬 환경과 충돌하지 않고 구체적인 데이터 저장소 버전을 실행하는 로컬 컨테이너를 설정하는 것이 정말 간단합니다. 실제 데이터 저장소를 사용하고 이에 대해 테스트를 실행하는 것이 저렴합니다.

    간단한 솔루션은 일반적으로 테스트 스위트를 실행하기 전에 미리 컨테이너를 실행하는 것이지만 github.com/ory/dockertest 덕분에 각 테스트 케이스 전에 컨테이너를 실제로 실행하기 쉽습니다.
    ory/dockertest Docker를 사용하여 컨테이너를 실행하고 관리합니다. 작동 방식은 API를 사용하여 배후에서 상호 작용하여 필요한 컨테이너를 적절하게 설정하는 것입니다. 이 경우에는 memcached:1.6.9-alpine 가 필요하므로 다음과 같은 기능을 구현합니다. 다음이 작동해야 합니다.

    func newClient(tb testing.TB) *memcache.Client {
        pool, err := dockertest.NewPool("")
        if err != nil {
            tb.Fatalf("Could not instantiate docker pool: %s", err)
        }
    
        pool.MaxWait = 2 * time.Second
    
        // 1. Define configuration options for the container to run.
    
        resource, err := pool.RunWithOptions(&dockertest.RunOptions{
            Repository: "memcached",
            Tag:        "1.6.6-alpine",
        }, func(config *docker.HostConfig) {
            config.AutoRemove = true
            config.RestartPolicy = docker.RestartPolicy{
                Name: "no",
            }
        })
    
        if err != nil {
            tb.Fatalf("Could not run container: %s", err)
        }
    
        addr := fmt.Sprintf("%s:11211", resource.Container.NetworkSettings.IPAddress)
        if runtime.GOOS == "darwin" { // XXX: network layer is different on Mac
            addr = net.JoinHostPort(resource.GetBoundIP("11211/tcp"), resource.GetPort("11211/tcp"))
        }
    
        // 2. Wait until the container is available and instantiate the actual client
        //    the value set above in `pool.MaxWait` determines how long it should wait.
    
        if err := pool.Retry(func() error {
            var ss memcache.ServerList
            if err := ss.SetServers(addr); err != nil {
                return err
            }
    
            return memcache.NewFromSelector(&ss).Ping()
        }); err != nil {
            tb.Fatalf("Could not connect to memcached: %s", err)
        }
    
        tb.Cleanup(func() {
            // 3. Get rid of the containers previously launched.
    
            if err := pool.Purge(resource); err != nil {
                tb.Fatalf("Could not purge container: %v", err)
            }
        })
    
        return memcache.New(addr)
    }
    


    이것은 테스트 스위트 또는 테스트 케이스당 새로운 컨테이너를 실행합니다(테스트 실행 계획에 따라 다름). 초기화를 통해 테스트 중에 사용할 memcached 도커 컨테이너를 실제로 실행할 수 있으며, 필요한 유형을 다룹니다. memcached.Client 뿐만 아니라:

    func TestConcreteMemcached(t *testing.T) {
        // TODO: Test sad/unhappy-paths cases
    
        client := newClient(t)
    
        c := mocking.NewConcreteMemcached(client)
    
        if err := c.Set("concrete-key", "value"); err != nil {
            t.Fatalf("expected no error, got %s", err)
        }
    
        value, err := c.Get("concrete-key")
        if err != nil {
            t.Fatalf("expected no error, got %s", err)
        }
    
        if value != "value" {
            t.Fatalf("expected `value`, got %s", value)
        }
    }
    


    마지막 생각들



    Go에서 데이터 저장소로 작업할 때 github.com/ory/dockertest을 사용하지 않을 변명의 여지가 없습니다. ory/dockertest 통합 테스트를 단순화하고 테스트를 실행할 때 필요한 종속성을 줄입니다. 이러한 경우에는 Docker만 필요하기 때문입니다.

    그러나 우리는 조심해야 하고 그것을 남용하지 않아야 합니다. 비록 우리가 말 그대로 하위 테스트당 하나의 컨테이너를 생성할 수 있는 옵션이 있지만 결국에는 최선의 아이디어가 아닐 수 있다는 점을 명심해야 합니다. 테스트 테스트 기간을 측정하여 제품군에 필요한 최상의 컨테이너 수를 결정해야 합니다. 시작하는 것이 좋습니다. 각 테스트 사례에 대해 하나씩 사용하고 격리에 따라 하위 테스트 수와 일치하도록 해당 값을 늘릴 수 있습니다. .

    결국ory/dockertest은 필수, 적극 권장합니다.

    좋은 웹페이지 즐겨찾기