英文:
How can I simulate a failure of rand.Reader for testing purposes?
问题
我正在尝试为我的函数编写单元测试;请注意,它调用了rand.Read
(来自crypto/rand包),该函数依赖于rand.Reader
:
func GenerateBytes(length int) ([]byte, error) {
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return nil, err
}
return bytes, nil
}
我能够为正常情况编写测试,但是我无法测试rand.Read
返回错误的情况... 有没有办法模拟rand.Read
失败的情况?
英文:
I am trying to write a unit test for my function; note that it calls rand.Read
(from the crypto/rand package), which relies on rand.Reader
:
func GenerateBytes(length int) ([]byte, error) {
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return nil, err
}
return bytes, nil
}
I am able to write tests for the happy path, but I'm unable to test the case where rand.Read
returns an error... Is there a way to simulate a failure of rand.Read
?
答案1
得分: 3
你究竟想要检查什么?是要检查如果rand.Read
返回一个非nil的错误,那么GenerateBytes
也会返回一个非nil的错误吗?你只是想要将覆盖率百分比提高到某个任意的阈值吗,还是这样的检查对你来说真的很重要?如果是前一种情况,请重新考虑。
如果是后一种情况,请注意直接依赖于像rand.Reader
这样的包级单例会使测试变得困难。提高可测试性需要一些额外的(但不是禁止的)努力。
一种可能的方法
幸运的是,rand.Reader
已经满足了一个方便的接口:io.Reader
。如果没有这样的接口可用,也没关系;你可以声明自己的自定义接口。这就是Go接口隐式满足的好处!
选择一种方式将io.Reader
传递给GenerateBytes
。最惯用的方式是将GenerateBytes
声明为某个自定义结构类型(下面命名为SourceOfRandomness
)的方法,该结构类型具有一个(可能是匿名的)io.Reader
字段:
type SourceOfRandomness struct {
Reader io.Reader
}
这样做可以让你在实例化自定义类型时选择要注入的io.Reader
实现。然后,GenerateBytes
可以通过其接收器访问所需的io.Reader
:
func (sor *SourceOfRandomness) GenerateBytes(length int) ([]byte, error) {
bytes := make([]byte, length)
if _, err := sor.Reader.Read(bytes); err != nil {
return nil, err
}
return bytes, nil
}
然后,在你的生产代码中,你可以使用实际的rand.Reader
初始化一个SourceOfRandomness
值:
sor := SourceOfRandomness{Reader: rand.Reader}
sor.GenerateBytes(42)
对于你考虑的特定测试用例,你可以简单地利用(如他的评论中所提到的 mh-cbon)iotest.ErrReader
,它返回一个Read
方法无条件失败的io.Reader
:
import (
"crypto/rand"
"testing/iotest"
)
func TestGenerateBytesFails(t *testing.T) {
// Arrange
sor := SourceOfRandomness{
Reader: iotest.ErrReader(errors.New("oops")),
}
const dummyLength = 42
// Act
_, err := sor.GenerateBytes(dummyLength)
// Assert
if err != nil {
t.Error("want non-nil error; got nil error")
}
}
英文:
What exactly are you trying to check? That, if rand.Read
returns a non-nil error, then so does GenerateBytes
? Are you simply trying to get the coverage percentage above some arbitrary threshold, or is such a check actually important to you? In the former case, please reconsider.
In the latter case, be aware that depending directly on package-level singletons like rand.Reader
makes testing difficult. Improving testability requires some additional (but not prohibitive) effort.
One possible approach
Thankfully, there's already a convenient interface that rand.Reader
satisfies: io.Reader
. If no such interface were already available, though, no biggie; you could have declared your own custom one. That's what's great about Go interfaces' implicit satisfaction!
Choose a way to pass an io.Reader
to GenerateBytes
. The most idiomatic way is to declare GenerateBytes
, not as a top-level function, but as a method on some custom struct type (named SourceOfRandomness
below) that has a (possibly anonymous) io.Reader
field:
type SourceOfRandomness struct {
Reader io.Reader
}
Doing so allows you to choose which io.Reader
implementor to inject at instantiation of your custom type. GenerateBytes
can then access the io.Reader
it needs through its receiver:
func (sor *SourceOfRandomness) GenerateBytes(length int) ([]byte, error) {
bytes := make([]byte, length)
if _, err := sor.Reader.Read(bytes); err != nil {
return nil, err
}
return bytes, nil
}
Then, in your production code, you'd initialise a SourceOfRandomness
value with the actual rand.Reader
:
sor := SourceOfRandomness{Reader: rand.Reader}
sor.GenerateBytes(42)
For the specific test case you had in mind, you can simply leverage (as mentioned by mh-cbon in his comment) iotest.ErrReader
, which returns an io.Reader
whose Read
method unconditionally fails:
import (
"crypto/rand"
"testing/iotest"
)
func TestGenerateBytesFails(t *testing.T) {
// Arrange
sor := SourceOfRandomness{
Reader: iotest.ErrReader(errors.New("oops"))
}
const dummyLength = 42
// Act
_, err := sor.GenerateBytes(dummyLength)
// Assert
if err != nil {
t.Error("want non-nil error; got nil error")
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论