如何对从包中导入的方法进行存根和间谍活动?

Posted

技术标签:

【中文标题】如何对从包中导入的方法进行存根和间谍活动?【英文标题】:How to make stubs and spies on methods which import from a package? 【发布时间】:2020-09-09 03:08:08 【问题描述】:

我是一名 javascript 和 Python 开发人员。这是一个使用jestjs测试框架的单元测试代码sn-p:

index.ts:

import dotenv from 'dotenv';

export class OsEnvFetcher 
  constructor() 
    const output = dotenv.config();
    if (output.error) 
      console.log('Error loading .env file');
      process.exit(1);
    
  

index.test.ts:

import  OsEnvFetcher  from './';
import dotenv from 'dotenv';

describe('OsEnvFetcher', () => 
  afterEach(() => 
    jest.restoreAllMocks();
  );
  it('should pass', () => 
    const mOutput =  error: new Error('parsed failure') ;
    jest.spyOn(dotenv, 'config').mockReturnValueOnce(mOutput);
    const errorLogSpy = jest.spyOn(console, 'log');
    const exitStub = jest.spyOn(process, 'exit').mockImplementation();
    new OsEnvFetcher();
    expect(dotenv.config).toBeCalledTimes(1);
    expect(errorLogSpy).toBeCalledWith('Error loading .env file');
    expect(exitStub).toBeCalledWith(1);
  );
);

单元测试的结果:

 PASS  ***/todo/index.test.ts (11.08s)
  OsEnvFetcher
    ✓ should pass (32ms)

  console.log
    Error loading .env file

      at CustomConsole.<anonymous> (node_modules/jest-environment-enzyme/node_modules/jest-mock/build/index.js:866:25)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------|---------|----------|---------|---------|-------------------
All files |     100 |       50 |     100 |     100 |                   
 index.ts |     100 |       50 |     100 |     100 | 6                 
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        12.467s

示例中的测试方法在使用Arrange, Act, Assert模式的js项目中很常见。由于dotenv.config() 方法会做一些文件系统I/O 操作,它有一个副作用。所以我们将为它制作一个存根或模拟。这样一来,我们的单元测试就没有副作用了,并且是在一个隔离的环境中进行测试的。

这同样适用于 python。我们可以使用unittest.mock 模拟对象库来做同样的事情。我对这些单元测试方法很满意。

现在,我转行,尝试做同样的事情。代码在这里:

osEnvFetcher.go

package util

import (
    "log"
    "os"
    "github.com/joho/godotenv"
)

var godotenvLoad = godotenv.Load

type EnvFetcher interface 
    Getenv(key string) string


type osEnvFetcher struct 

func NewOsEnvFetcher() *osEnvFetcher 
    err := godotenvLoad()
    if err != nil 
        log.Fatal("Error loading .env file")
    
    return &osEnvFetcher


func (f *osEnvFetcher) Getenv(key string) string 
    return os.Getenv(key)

osEnvFetcher_test.go:

package util

import (
    "testing"
    "fmt"
)

func TestOsEnvFetcher(t *testing.T) 
    old := godotenvLoad
    defer func()  godotenvLoad = old ()
    godotenvLoad = func() error 
        return 
    
    osEnvFetcher := NewOsEnvFetcher()
    port := osEnvFetcher.Getenv("PORT")
    fmt.Println(port)

测试用例没有完成。我不确定我应该如何模拟、存根或窥探godotenv.Load 方法(相当于dotenv.config())和log.Fatal 方法?我找到了这个模拟包 - mock。但是godotenv包没有接口,它是由函数组成的。

我正在寻找jest.mock(moduleName, factory, options) 和jest.spyOn(object, methodName) 之类的方法。或者,sinonjs 中的stubs 和spies。或者,像 spyOn 或 jasmine。这些方法几乎可以涵盖任何测试场景。无论是使用 DI 还是直接导入模块。

我看到了一些方法,但它们都有自己的问题。

例如https://***.com/a/41661462/6463558.

如果我需要 stub 十个有副作用的方法呢?在运行测试之前,我需要将一个包的这些方法分配给 10 个变量,并用测试用例中的 mocked 版本替换它们十次。它是不可扩展的。也许我可以创建一个__mocks__ 目录并将所有模拟版本对象放入其中。这样我就可以在所有测试文件中使用它们。

使用依赖注入更好。通过这种方式,我们可以将模拟对象传递给函数/方法。但有些场景是直接导入包,使用包的方法(第3个或内置标准库)。这也是我的问题中的场景。我认为这种情况是不可避免的,肯定会出现在代码的某一层。

我可以使用jestjs 轻松处理这种情况,例如

util.js,

exports.resolveAddress = function(addr) 
  // ...
  const data = exports.parseJSON(json);
  return data;

exports.parseJSON = function(json) 

main.js:

// import util module directly rather than using Dependency Injection
import util from './util';
function main() 
  return util.resolveAddress();

main.test.js:

import util from './util';
const mJson = 'mocked json data';
jest.spyOn(util, 'parseJSON').mockReturnValueOnce(mJson)
const actual = main()
expect(actual).toBe('mocked json data');

我直接导入util 模块并模拟util.parseJSON 方法及其返回值。我不确定 Go 的包是否可以做到这一点。目前,这些问题是安排问题。

此外,我还需要检查方法是否确实被调用,以确保代码逻辑和分支正确。 (例如,使用 jestjs.toBeCalledWith() 方法)。这是断言问题。

提前致谢!

【问题讨论】:

你当然不能模拟/存根/等。 log.Fatal,那是a函数,不可篡改,不能与其他函数体切换。另一方面,godotenvLoad 只是一个变量,您可以将其值替换为您想要的任何值,只要它的类型与变量的类型匹配即可。 在 Go 中 mocking 并不常见,而 fakes 和 stubs 是测试具有依赖关系的东西的正常方法。这通常会导致更清晰的测试。 【参考方案1】:

这是我基于answer的解决方案:

osEnvFetcher.go:

package util

import (
    "log"
    "os"
    "github.com/joho/godotenv"
)

var godotenvLoad = godotenv.Load
var logFatal = log.Fatal

type EnvFetcher interface 
    Getenv(key string) string


type osEnvFetcher struct 

func NewOsEnvFetcher() *osEnvFetcher 
    err := godotenvLoad()
    if err != nil 
        logFatal("Error loading .env file")
    
    return &osEnvFetcher


func (f *osEnvFetcher) Getenv(key string) string 
    return os.Getenv(key)

osEnvFetcher_test.go:

package util

import (
    "testing"
    "errors"
)


func mockRestore(oGodotenvLoad func(...string) error, oLogFatal func(v ...interface)) 
    godotenvLoad = oGodotenvLoad
    logFatal = oLogFatal


func TestOsEnvFetcher(t *testing.T) 
    // Arrange
    oGodotenvLoad := godotenvLoad
    oLogFatal := logFatal
    defer mockRestore(oGodotenvLoad, oLogFatal)
    var godotenvLoadCalled = false
    godotenvLoad = func(...string) error 
        godotenvLoadCalled = true
        return errors.New("parsed failure")
    
    var logFatalCalled = false
    var logFatalCalledWith interface
    logFatal = func(v ...interface) 
        logFatalCalled = true
        logFatalCalledWith = v[0]
    
    // Act
    NewOsEnvFetcher()
    // Assert
    if !godotenvLoadCalled 
        t.Errorf("godotenv.Load should be called")
    
    if !logFatalCalled 
        t.Errorf("log.Fatal should be called")
    
    if logFatalCalledWith != "Error loading .env file" 
        t.Errorf("log.Fatal should be called with: %s", logFatalCalledWith)
    

覆盖测试的结果:

☁  util [master] ⚡  go test -v -coverprofile cover.out
=== RUN   TestOsEnvFetcher
--- PASS: TestOsEnvFetcher (0.00s)
PASS
coverage: 80.0% of statements

报道 html 记者:

【讨论】:

以上是关于如何对从包中导入的方法进行存根和间谍活动?的主要内容,如果未能解决你的问题,请参考以下文章

xml Spock模拟存根间谍

等效的Answers.RETURNS_DEEP_STUBS为在mockito中的间谍

间谍使用导出的功能

Mockito 无法创建 @Autowired Spring-Data Repository 的间谍

模拟框架中的模拟与间谍活动

如何隐藏滚动间谍中的其他内容而不是滚动到特定位置