使用testify mock进行表驱动测试

huangapple go评论83阅读模式
英文:

Table driven tests with testify mock

问题

有没有使用testify编写干净的表驱动测试的示例?对于输入和预期输出的表驱动测试很好用,但是测试来自依赖项的输出似乎很难做到。

下面的示例使用了一个模拟接口,并要求我编写一个全新的测试函数来验证被测试函数正确处理依赖项错误。
我只是想寻求一些建议,以使使用testify模拟包编写单元测试更加简洁。

package packageone

import (
	"errors"
	"musings/packageone/mocks"
	"testing"
)

//常规的表驱动测试
func TestTstruct_DoSomething(t *testing.T) {
	testObj := new(mocks.Dinterface)

	passes := []struct {
		Input  int
		Output int
	}{{0, 0}, {1, 1}, {2, 4}, {100, 10000}}

	for _, i := range passes {
		testObj.On("DoSomethingWithD", i.Input).Return(i.Output, nil)
	}

	type fields struct {
		DC Dinterface
	}
	type args struct {
		i int
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		wantRes int
		wantErr bool
	}{
		{"Pass#0", fields{testObj}, args{passes[0].Input}, passes[0].Output, false},
		{"Pass#1", fields{testObj}, args{passes[1].Input}, passes[1].Output, false},
		{"Pass#2", fields{testObj}, args{passes[2].Input}, passes[2].Output, false},
		{"Pass#3", fields{testObj}, args{passes[3].Input}, passes[3].Output, false},
		{"Fail#4", fields{testObj}, args{-1}, 0, true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			r := &Tstruct{
				DC: tt.fields.DC,
			}
			gotRes, err := r.DoSomething(tt.args.i)
			if (err != nil) != tt.wantErr {
				t.Errorf("Tstruct.DoSomething() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if gotRes != tt.wantRes {
				t.Errorf("Tstruct.DoSomething() = %v, want %v", gotRes, tt.wantRes)
			}
		})
	}
}

//单独的依赖项返回错误的单元测试
func TestTstruct_ErrMock_DoSomething(t *testing.T) {
	testObj := new(mocks.Dinterface)
	testObj.On("DoSomethingWithD", 1).Return(0, errors.New(""))

	type fields struct {
		DC Dinterface
	}
	type args struct {
		i int
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		wantRes int
		wantErr bool
	}{
		{"Test#1", fields{testObj}, args{1}, 0, true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			r := &Tstruct{
				DC: tt.fields.DC,
			}
			gotRes, err := r.DoSomething(tt.args.i)
			if (err != nil) != tt.wantErr {
				t.Errorf("Tstruct.DoSomething() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if gotRes != tt.wantRes {
				t.Errorf("Tstruct.DoSomething() = %v, want %v", gotRes, tt.wantRes)
			}
		})
	}
}

以上是使用testify进行表驱动测试的示例代码。希望对你有所帮助!

英文:

Are there any examples of writing clean table driven tests using testify. A table driven test for input and expected output works well but having to test the output from a dependency seems to be really hard to do.

The below example uses one mocked interface and requires me to write a whole new test function to verify the function under test handles dependency errors properly.
I am only looking for suggestions to make writing unit tests with the testify mock package more streamlined.

package packageone
import (
"errors"
"musings/packageone/mocks"
"testing"
)
//Regular Table driven test
func TestTstruct_DoSomething(t *testing.T) {
testObj := new(mocks.Dinterface)
passes := []struct {
Input  int
Output int
}{{0, 0}, {1, 1}, {2, 4}, {100, 10000}}
for _, i := range passes {
testObj.On("DoSomethingWithD", i.Input).Return(i.Output, nil)
}
type fields struct {
DC Dinterface
}
type args struct {
i int
}
tests := []struct {
name    string
fields  fields
args    args
wantRes int
wantErr bool
}{
{"Pass#0", fields{testObj}, args{passes[0].Input}, passes[0].Output, false},
{"Pass#1", fields{testObj}, args{passes[1].Input}, passes[1].Output, false},
{"Pass#2", fields{testObj}, args{passes[2].Input}, passes[2].Output, false},
{"Pass#3", fields{testObj}, args{passes[3].Input}, passes[3].Output, false},
{"Fail#4", fields{testObj}, args{-1}, 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Tstruct{
DC: tt.fields.DC,
}
gotRes, err := r.DoSomething(tt.args.i)
if (err != nil) != tt.wantErr {
t.Errorf("Tstruct.DoSomething() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotRes != tt.wantRes {
t.Errorf("Tstruct.DoSomething() = %v, want %v", gotRes, tt.wantRes)
}
})
}
}
//Separate Unit test for dependency returning errors.
func TestTstruct_ErrMock_DoSomething(t *testing.T) {
testObj := new(mocks.Dinterface)
testObj.On("DoSomethingWithD", 1).Return(0, errors.New(""))
type fields struct {
DC Dinterface
}
type args struct {
i int
}
tests := []struct {
name    string
fields  fields
args    args
wantRes int
wantErr bool
}{
{"Test#1", fields{testObj}, args{1}, 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Tstruct{
DC: tt.fields.DC,
}
gotRes, err := r.DoSomething(tt.args.i)
if (err != nil) != tt.wantErr {
t.Errorf("Tstruct.DoSomething() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotRes != tt.wantRes {
t.Errorf("Tstruct.DoSomething() = %v, want %v", gotRes, tt.wantRes)
}
})
}
}

答案1

得分: 6

编写单元测试相对容易,编写好的单元测试却很难。这是因为我们在介绍单元测试时通常使用的是不模拟真实使用情况的简单代码示例。

除非需要验证依赖项的调用,否则尽量避免使用模拟。更好的选择是使用存根(stubs)、伪对象(fakes)或真实实现。何时使用每种方法取决于经验,也是困难所在。此外,要考虑你的设计。如果你发现很难进行单元测试,可能是因为需要重新设计。

编写和维护单元测试需要时间。编写没有单元测试的代码会更快。然而,我们编写单元测试是为了确保代码正确运行并增加重构的信心。

因此,重要的是尽量针对行为(黑盒)编写测试,而不是针对实现(白盒)编写测试。虽然这并不总是可能的,但与实现绑定的单元测试是脆弱的,会阻碍重构,并且有时也会掩盖意外行为。

一些值得阅读的单元测试资源:

  1. Mocks Aren't Stubs
  2. Testing on the Toilet Blog
  3. TDD - Where it all went wrong

以一个简单的电子邮件地址验证器的单元测试为例。我们想要编写一个函数,根据提供的字符串返回 true/false,表示是否提供了有效的电子邮件地址。

一个简单的示例实现可能是:

var re = regexp.MustCompile("[regular expression]")
func ValidateEmail(s string) bool {
   return re.MatchString(s)
}

然后,我们会编写一个带有各种输入的表驱动测试,例如 ""good@example.combad 等,并验证结果是否正确。

现在这只是一个简单的示例,但它说明了我的观点。有人可能会说这很容易,因为该函数没有依赖性,但实际上它是有依赖性的!我们依赖于正则表达式的实现和我们传递给它的正则表达式。

这是在测试所需行为,而不是如何实现它。我们不关心它如何验证电子邮件地址,只要它能验证就可以。如果我们调整正则表达式或完全更改实现,只有结果不正确时,这些测试才会失败

很少有人会建议我们隔离依赖项,并通过模拟正则表达式来测试验证函数,并确保它使用我们期望的正则表达式进行调用。这样做更加脆弱,但也没有太多用处,也就是说,我们怎么知道正则表达式实际上会起作用呢?


对于你的具体示例,你可以轻松避免使用模拟,而使用一个简单的伪对象来测试正常结果和错误情况。示例如下:

// 用于测试错误结果
var errFail = errors.New("Failed")

// 伪对象类型
type fakeD func(input int) (int, error)

// 实现 Dinterface
func (f fakeD) DoSomethingWithD(input int) (int, error) {
    return f(input)
}

// 伪对象实现。在输入为 5 时返回错误,否则返回输入的平方
var fake fakeD = func(input int) (int, error) {
    if input == 5 {
        return nil, errFail
    }
    return input * input, nil
}

然后,只需将 fake 作为你的依赖项,并像平常一样运行基于表的测试。

英文:

Writing unit tests is relatively easy. Writing good unit tests is hard. This isn't helped because we are introduced to unit testing with trivial code examples that don't mimic real life usage.

Try to avoid mocking unless you need to verify the invocations of a dependency. Prefer using stubs, fakes or real implementations. Knowing when to use each is a matter of experience and where the difficulty comes in. Also, think about your design. If you are finding it difficult to unit test, this could be because you need to redesign.

Unit tests take time to write and maintain. You will always be quicker writing code without unit tests. However, we write unit tests to give us some assurance that our code works correctly and confidence to re-factor.

Hence it's important to try to write the test against the behaviour (black box) instead of the implementation (white-box). This isn't always possible but unit tests that are tied to the implementation are fragile, discourage refactoring and can also sometimes mask unexpected behaviour.

Some unit testing resources worth reading:

  1. Mocks Aren't Stubs
  2. Testing on the Toilet Blog
  3. TDD - Where it all went wrong

As way as an example, think of writing a unit test for a simple email address validator. We want to write a function that will take a string and return true/false based on whether a valid email address was supplied.

A trivial example implementation would be:

var re = regexp.MustCompile("[regular expression]")
func ValidateEmail(s string) bool {
return re.MatchString(s)
}

We would then write a table driven test with the various inputs, e.g. "", good@example.com, bad etc and verify the result was correct.

Now this is a bit of a trivial example but illustrates my point. One may argue that this is easy because the function has no dependencies but it does! We are relying on the regexp implementation and the regular expression we are passing it.

This is testing the desired behaviour, not how we implement it. We don't care how it validates an email address, simply that it does. If we were to tweak the regular expression or completely change the implementation then none of this would break the tests unless the result was incorrect.

Very few would suggest that we should isolate the dependency and test the validation function by mocking the regexp and ensuring that it is called with the regular expression we expect. This would be far more fragile but also less useful, i.e. how would we know the regular expression is actually going to work?


For your specific example, you could easily avoid mocking & use a trivial fake to test both normal results and the error cases. This would be something like:

// Used to test error result, 
var errFail = errors.New("Failed")
// Fake type
type fakeD func(input int) (int, error)
// Implements Dinterface
func (f fakeD) DoSomethingWithD(input int) (int, error) {
return f(input)
}
// Fake implementation. Returns error on input 5, otherwise input * input
var fake fakeD = func(input int) (int, error) {
if input == 5 {
return nil, errFail
}
return input * input, nil
}

Then simply use fake as your dependency and run your table based tests as normal.

huangapple
  • 本文由 发表于 2017年8月7日 07:03:18
  • 转载请务必保留本文链接:https://go.coder-hub.com/45537444.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定