I am a JavaScript and Python developer. Here is a unit testing code snippet using jestjs testing framework:
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);
});
});
The outcome for the unit testing:
PASS stackoverflow/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
The test method in the example is very common in a js project using Arrange, Act, Assert pattern. Due to the dotenv.config()
method will do some file system I/O operations, it has a side-effect. So we will make a stub or mock for it. In this way, our unit tests have no side effects and are tested in an isolated environment.
The same applies to python. We can use unittest.mock mock object library to do the same thing. I'm very comfortable with these unit testing approaches.
Now, I switch to go, try to do the same thing. Code here:
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)
}
The test case is not finished. I'm not sure how should I mock, stub, or spy godotenv.Load
method(Equivalent to dotenv.config()
) and log.Fatal
method? I found this mock package - mock. But godotenv package has no interface, it's composed by functions.
I'm looking for some ways like jest.mock(moduleName, factory, options) and jest.spyOn(object, methodName).Or, like stubs and spies of sinonjs. Or, like spyOn of jasmine. These methods can cover almost any test scenario. No matter use DI or import modules directly.
I saw some ways, but they all have their own problems.
E.g. https://stackoverflow.com/a/41661462/6463558.
What about I need to stub ten methods which have side-effect? I need to assign these methods of a package to 10 variables and replace them with the mocked version in test cases ten times before running the test. It's non-scalable. Maybe I can create a __mocks__
directory and put all the mocked version object in it. So that I can use them in all test files.
Use Dependency Injection is better. In this way we can pass the mocked objects to the function/method. But some scenarios are directly importing the packages and use the methods of packages(3rd or built-in standard library). This is also the scenario in my question. I think this scenario is inevitable and will definitely appear on a certain layer of the code.
I can handle this scenario easily using jestjs
, E.g.
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');
I directly imports util
module and mock util.parseJSON
method and its returned value. I am not sure if Go's package can do this. For now, these issues are Arrange issues.
Besides, I also need to check the methods are indeed called to make sure the code logics and branches are correct. (E.g. the .toBeCalledWith()
method using jestjs
). This is Assert issue.
Thanks in advance!