如何对从包中导入的方法进行存根和监视?
How to make stubs and spies on methods which import from a package?
我是 JavaScript 和 Python 开发人员。这是使用 jestjs 测试框架的单元测试代码片段:
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 Whosebug/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. But godotenv 包没有接口,它是由函数组成的。
我正在寻找类似 jest.mock(moduleName, factory, options) and jest.spyOn(object, methodName).Or, like stubs and spies of sinonjs. Or, like spyOn of jasmine 的方法。这些方法几乎可以涵盖任何测试场景。无论使用DI还是直接导入模块。
我看到了一些方法,但是它们都有各自的问题。
例如.
我需要存根十个有副作用的方法怎么办?我需要将一个包的这些方法分配给10个变量,并在测试前十次用测试用例中的mock版本替换它们运行。它是不可扩展的。也许我可以创建一个 __mocks__
目录并将所有模拟版本对象放入其中。这样我就可以在所有测试文件中使用它们。
使用依赖注入更好。通过这种方式,我们可以将模拟对象传递给 function/method。但是有些场景是直接导入包并使用包的方法(第三个或内置标准库)。这也是我问题中的场景。我觉得这种场景是不可避免的,一定会出现在代码的某一层。
我可以使用 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 的包是否可以做到这一点。目前,这些问题是 安排 个问题。
此外,我还需要检查是否确实调用了方法,以确保代码逻辑和分支是正确的。 (例如 .toBeCalledWith()
方法使用 jestjs
)。这是 断言 问题。
提前致谢!
这是我基于此 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记者:
我是 JavaScript 和 Python 开发人员。这是使用 jestjs 测试框架的单元测试代码片段:
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 Whosebug/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. But godotenv 包没有接口,它是由函数组成的。
我正在寻找类似 jest.mock(moduleName, factory, options) and jest.spyOn(object, methodName).Or, like stubs and spies of sinonjs. Or, like spyOn of jasmine 的方法。这些方法几乎可以涵盖任何测试场景。无论使用DI还是直接导入模块。
我看到了一些方法,但是它们都有各自的问题。
例如.
我需要存根十个有副作用的方法怎么办?我需要将一个包的这些方法分配给10个变量,并在测试前十次用测试用例中的mock版本替换它们运行。它是不可扩展的。也许我可以创建一个 __mocks__
目录并将所有模拟版本对象放入其中。这样我就可以在所有测试文件中使用它们。
使用依赖注入更好。通过这种方式,我们可以将模拟对象传递给 function/method。但是有些场景是直接导入包并使用包的方法(第三个或内置标准库)。这也是我问题中的场景。我觉得这种场景是不可避免的,一定会出现在代码的某一层。
我可以使用 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 的包是否可以做到这一点。目前,这些问题是 安排 个问题。
此外,我还需要检查是否确实调用了方法,以确保代码逻辑和分支是正确的。 (例如 .toBeCalledWith()
方法使用 jestjs
)。这是 断言 问题。
提前致谢!
这是我基于此 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记者: