douyakao5308
2018-06-27 14:33
浏览 185
已采纳

如何在Golang中进行单元测试时测试是否已调用goroutine?

suppose that we have a method like this:

func method(intr MyInterface) {
    go intr.exec()
} 

In unit testing method, we want to assert that inter.exec has been called once and only once; so we can mock it with another mock struct in tests, which will give us functionality to check if it has been called or not:

type mockInterface struct{
    CallCount int
}

func (m *mockInterface) exec() {
    m.CallCount += 1
}

And in unit tests:

func TestMethod(t *testing.T) {
    var mock mockInterface{}
    method(mock)
    if mock.CallCount != 1 {
        t.Errorf("Expected exec to be called only once but it ran %d times", mock.CallCount)
    }
}

Now, the problem is that since intr.exec is being called with go keyword, we can't be sure that when we reach our assertion in tests, it has been called or not.

Possible Solution 1:

Adding a channel to arguments of intr.exec may solve this: we could wait on receiving any object from it in tests, and after receiving an object from it then we could continue to assert it being called. This channel will be completely unused in production (non-test) codes. This will work but it adds unnecessary complexity to non-test codes, and may make large codebases incomprehensible.

Possible Solution 2:

Adding a relatively small sleep to tests before assertion may give us some assurance that the goroutine will be called before sleep is finished:

func TestMethod(t *testing.T) {
    var mock mockInterface{}
    method(mock)

    time.sleep(100 * time.Millisecond)

    if mock.CallCount != 1 {
        t.Errorf("Expected exec to be called only once but it ran %d times", mock.CallCount)
    }
}

This will leave non-test codes as they are now.
The problem is that it will make tests slower, and will make them flaky, since they may break in some random circumstances.

Possible Solution 3:

Creating a utility function like this:

var Go = func(function func()) {
    go function()
} 

And rewrite method like this:

func method(intr MyInterface) {
    Go(intr.exec())
} 

In tests, we could change Go to this:

var Go = func(function func()) {
    function()
} 

So, when we're running tests, intr.exec will be called synchronously, and we can be sure that our mock method is called before assertion.
The only problem of this solution is that it's overriding a fundamental structure of golang, which is not right thing to do.


These are solutions that I could find, but non are satisfactory as far as I can see. What is best solution?

  • 写回答
  • 关注问题
  • 收藏
  • 邀请回答

3条回答 默认 最新

  • duanqi6274 2018-06-27 14:47
    已采纳

    Use a sync.WaitGroup inside the mock

    You can extend mockInterface to allow it to wait for the other goroutine to finish

    type mockInterface struct{
        wg sync.WaitGroup // create a wait group, this will allow you to block later
        CallCount int
    }
    
    func (m *mockInterface) exec() {
        m.wg.Done() // record the fact that you've got a call to exec
        m.CallCount += 1
    }
    
    func (m *mockInterface) currentCount() int {
        m.wg.Wait() // wait for all the call to happen. This will block until wg.Done() is called.
        return m.CallCount
    }
    

    In the tests you can do:

    mock := &mockInterface{}
    mock.wg.Add(1) // set up the fact that you want it to block until Done is called once.
    
    method(mock)
    
    if mock.currentCount() != 1 {  // this line with block
        // trimmed
    }
    
    打赏 评论
  • dousigan0499 2018-06-27 16:46

    first of all I would use a mock generator, i.e. github.com/gojuno/minimock instead of writing mocks yourself:

    minimock -f example.go -i MyInterface -o my_interface_mock_test.go

    then your test can look like this (btw the test stub is also generated with github.com/hexdigest/gounit)

    func Test_method(t *testing.T) {
        type args struct {
            intr MyInterface
        }
        tests := []struct {
            name string
            args func(t minimock.Tester) args
        }{
            {
                name: "check if exec is called",
                args: func(t minimock.Tester) args {
                    return args{
                        intr: NewMyInterfaceMock(t).execMock.Return(),
                    }
                },
            },
        }
    
        for _, tt := range tests {
            t.Run(tt.name, func(t *testing.T) {
                mc := minimock.NewController(t)
                defer mc.Wait(time.Second)
    
                tArgs := tt.args(mc)
    
                method(tArgs.intr)
            })
        }
    }
    

    In this test

    defer mc.Wait(time.Second)
    

    Waits for all mocked methods to be called.

    打赏 评论
  • doufei2328 2018-06-28 05:42

    This test won't hang forever as with sync.WaitGroup solution proposed above. It will hang for a second (in this particular example) in case when there is no call to mock.exec:

    package main
    
    import (
        "testing"
        "time"
    )
    
    type mockInterface struct {
        closeCh chan struct{}
    }
    
    func (m *mockInterface) exec() {
        close(closeCh)
    }
    
    func TestMethod(t *testing.T) {
        mock := mockInterface{
            closeCh: make(chan struct{}),
        }
    
        method(mock)
    
        select {
        case <-closeCh:
        case <-time.After(time.Second):
            t.Fatalf("expected call to mock.exec method")
        }
    }
    

    This is basically what mc.Wait(time.Second) in my answer above.

    打赏 评论

相关推荐 更多相似问题