您好!您想知道如何模拟 rand.Reader 失败以进行测试目的。

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

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-cboniotest.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")
  }
}

huangapple
  • 本文由 发表于 2022年3月29日 17:08:36
  • 转载请务必保留本文链接:https://go.coder-hub.com/71659292.html
匿名

发表评论

匿名网友

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

确定