How to mock a call to logger.warn? - unit-testing

I'm practicing test-first development and I want to ensure that method in a class always calls my logger at the warn level with a message. My class is defined like so:
import { log4js } from '../config/log4js-config'
export const logger = log4js.getLogger('myClass')
class MyClass {
sum(numbers) {
const reducer = (accumulator, currentValue) => accumulator + currentValue
const retval = numbers.reduce(reducer))
if (retval < 0) {
logger.warn('The sum is less than zero!')
}
return retval
}
}
const myClass = new MyClass()
export { myClass }
My test looks like this:
import { myClass, logger } from './MyClass'
import { log4js } from '../config/log4js-config'
jest.mock('log4js')
describe('MyClass', () => {
it('logs a warn-level message if sum is negative', () => {
logger.warn = jest.fn()
logger._log = jest.fn()
myClass.sum([0, -1])
expect(logger.warn).toHaveBeenCalled() // <--- fails
expect(logger._log).toHaveBeenCalled() // <--- fails
})
})
I've also tried to mock log4js.Logger._log in the setup but that didn't seem to work either. 😕 Any suggestions are appreciated!

The thing with mocking is that you need to provide the mock, simplest method for me is through the mock factory. However i would recomend also some refactoring:
import { getLogger } from 'log4js'
export const logger = getLogger('myClass')
logger.level = 'debug'
// export the class itself to avoid memory leaks
export class MyClass {
// would consider even export just the sum function
sum(numbers) {
const reducer = (accumulator, currentValue) => accumulator + currentValue
const retval = numbers.reduce(reducer))
if (retval < 0) {
logger.warn('The sum is less than zero!')
}
return retval
}
}
import log4js from 'log4js';
import { MyClass } from "./class";
jest.mock('log4js', () => {
// using the mock factory we mimic the library.
// this mock function is outside the mockImplementation
// because we want to check the same mock in every test,
// not create a new one mock every log4js.getLogger()
const warn = jest.fn()
return {
getLogger: jest.fn().mockImplementation(() => ({
level: jest.fn(),
warn,
})),
}
})
beforeEach(() => {
// reset modules to avoid leaky scenarios
jest.resetModules()
})
// this is just some good habits, if we rename the module
describe(MyClass, () => {
it('logs a warn-level message if sum is negative', () => {
const myClass = new MyClass()
myClass.sum([0, -1])
// now we can check the mocks
expect(log4js.getLogger).toHaveBeenCalledTimes(1) // <--- passes
// check exactly the number of calls to be extra sure
expect(log4js.getLogger().warn).toHaveBeenCalledTimes(1) // <--- passes
})
})

Maybe simply spying on logger methods can do the trick
import { myClass, logger } from './MyClass'
describe('MyClass', () => {
it('logs a warn-level message if sum is negative', () => {
const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => {});
const _logSpy = jest.spyOn(logger, '_log').mockImplementation(() => {});
myClass.sum([0, -1])
expect(warnSpy).toHaveBeenCalled()
expect(_logSpy).toHaveBeenCalled()
})
})

Related

Nestjs unit test: TypeError: this.userModel.findById(...).exec is not a function

using nestjs framework and with a repository class that uses mongoose to do the CRUD operations we have a simple users.repository.ts file like this:
#Injectable()
export class UserRepository {
constructor(#InjectModel(User.name) private userModel: Model<UserDocument>) {}
async create(createUserInput: CreateUserInput) {
const createdUser = new this.userModel(createUserInput);
return await createdUser.save();
}
}
async findById(_id: MongooseSchema.Types.ObjectId) {
return await this.userModel.findById(_id).exec();
}
and it works normally when the server is up.
consider this users.repository.spec file :
import { Test, TestingModule } from '#nestjs/testing';
import { getModelToken } from '#nestjs/mongoose';
import { Model } from 'mongoose';
// User is my class and UserDocument is my typescript type
// ie. export type UserDocument = User & Document; <-- Mongoose Type
import { User, UserDocument } from '../domain/user.model';
import { UserRepository } from './users.repository';
//import graphqlScalars from 'graphql-scalar-types';
describe('UsersRepository', () => {
let mockUserModel: Model<UserDocument>;
let mockRepository: UserRepository;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: getModelToken(User.name),
useValue: Model, // <-- Use the Model Class from Mongoose
},
UserRepository,
//graphqlScalars,
],
}).compile();
// Make sure to use the correct Document Type for the 'module.get' func
mockUserModel = module.get<Model<UserDocument>>(getModelToken(User.name));
mockRepository = module.get<UserRepository>(UserRepository);
});
it('should be defined', () => {
expect(mockRepository).toBeDefined();
});
it('should return a user doc', async () => {
// arrange
const user = new User();
const userId = user._id;
const spy = jest
.spyOn(mockUserModel, 'findById') // <- spy
.mockResolvedValue(user as UserDocument); // <- set resolved value
// act
await mockRepository.findById(userId);
// assert
expect(spy).toBeCalled();
});
});
so my question:
for the should return a user doc test i get TypeError: metatype is not a constructor when and i guess
.mockResolvedValue(user as UserDocument);
should be fixed.
Note:graphql is used the query to the API and i have no idea that if the scalars should be provieded or not, if i uncomment the scalar, the expect(mockRepository).toBeDefined(); test would not pass any more
so any idea to fix the test would be apreciated.
to handle a chained .exec we should define it via mockReturnThis():
static findById = jest.fn().mockReturnThis();
I needed the constructor to be called via new so i preferd to define a mock class in this way:
class UserModelMock {
constructor(private data) {}
new = jest.fn().mockResolvedValue(this.data);
save = jest.fn().mockResolvedValue(this.data);
static find = jest.fn().mockResolvedValue(mockUser());
static create = jest.fn().mockResolvedValue(mockUser());
static remove = jest.fn().mockResolvedValueOnce(true);
static exists = jest.fn().mockResolvedValue(false);
static findOne = jest.fn().mockResolvedValue(mockUser());
static findByIdAndUpdate = jest.fn().mockResolvedValue(mockUser());
static findByIdAndDelete = jest.fn().mockReturnThis();
static exec = jest.fn();
static deleteOne = jest.fn().mockResolvedValue(true);
static findById = jest.fn().mockReturnThis();
}

mock socket.io-client with jest when socket used in an es6 class

I'm writing unit tests with jest and I want to mock the socket.io-client module so I can test an es6 class that's using it.
Here's a simplified version of the class:
import io from "socket.io-client"
export default class BotClient {
io: SocketIOClientStatic
client: SocketIOClient.Socket | undefined
connected: boolean = false
attemptReconnection: boolean = true
constructor() {
this.io = io
}
init() {
try {
this.reconnectToApi()
} catch (error) {
console.error(error)
}
}
reconnectToApi(): void {
const interval = setInterval(() => {
if (this.client?.connected || !this.attemptReconnection) {
this.connected = true
this.addHooks()
clearInterval(interval)
return
}
this.client = this.io.connect("myurl")
}, 5000)
}
And I wrote my test to mock the socket.io-client module and return a custom implementation object that returns a mock function for the connect method like this:
import BotClient from "../src/lib/BotClient"
import io from "socket.io-client"
const mockClient = {
connect: jest.fn(() => {
connected: true
}),
}
jest.mock("socket.io-client", () => {
return mockClient
})
test("client will attempt connection until told to stop", () => {
jest.useFakeTimers()
const reconnectSpy = jest.spyOn(client, "reconnectToApi")
const hookSpy = jest.spyOn(client, "addHooks")
client.init("my-bot-id")
const callCount = 3
jest.advanceTimersByTime(5000 * callCount)
expect(reconnectSpy).toHaveBeenCalledTimes(1)
expect(hookSpy).toHaveBeenCalledTimes(1)
})
But when I try to run the test I get the error:
TypeError: Cannot read property 'connect' of undefined
I'm looking over the jest docs on mocking modules and using custom implementations and it seems like I'm doing it right, but obviously I'm not :/ I feel like I'm tripping over information in the docs.
How would I do this correctly? What part am I misunderstanding?

How can I unit test whether the ChangeNotifier's notifyListeners was called in Flutter/Dart?

I'm using the provider package in our app and I want to test my ChangeNotifier class individually to have simple unit tests checking the business logic.
Apart from the values of ChangeNotifier properties, I also want to ensure that in certain cases (where necessary), the notifyListeners has been called, as otherwise, the widgets that rely on up-to-date information from this class would not be updated.
Currently, I'm indirectly testing whether the notifyListeners have been called: I'm using the fact that the ChangeNotifier lets me add a callback using its addListener method. In the callback that I add in our testing suite, I simply increment an integer counter variable and make assertions on that.
Is this the right way to test whether my ChangeNotifier calls its listeners? Is there a more descriptive way of testing this?
Here is the class I'm testing:
import 'package:flutter/foundation.dart';
class ExampleModel extends ChangeNotifier {
int _value = 0;
int get value => _value;
void increment() {
_value++;
notifyListeners();
}
}
and this is how I test it:
import 'package:mobile_app/example_model.dart';
import 'package:test/test.dart';
void main() {
group('$ExampleModel', () {
ExampleModel exampleModel;
int listenerCallCount;
setUp(() {
listenerCallCount = 0;
exampleModel = ExampleModel()
..addListener(() {
listenerCallCount += 1;
});
});
test('increments value and calls listeners', () {
exampleModel.increment();
expect(exampleModel.value, 1);
exampleModel.increment();
expect(listenerCallCount, 2);
});
test('unit tests are independent from each other', () {
exampleModel.increment();
expect(exampleModel.value, 1);
exampleModel.increment();
expect(listenerCallCount, 2);
});
});
}
Your approach seems fine to me but if you want to have a more descriptive way you could also use Mockito to register a mock callback function and test whether and how often the notifier is firing and thus notifying your registered mock instead of incrementing a counter:
import 'package:mobile_app/example_model.dart';
import 'package:test/test.dart';
/// Mocks a callback function on which you can use verify
class MockCallbackFunction extends Mock {
call();
}
void main() {
group('$ExampleModel', () {
late ExampleModel exampleModel;
final notifyListenerCallback = MockCallbackFunction(); // Your callback function mock
setUp(() {
exampleModel = ExampleModel()
..addListener(notifyListenerCallback);
reset(notifyListenerCallback); // resets your mock before each test
});
test('increments value and calls listeners', () {
exampleModel.increment();
expect(exampleModel.value, 1);
exampleModel.increment();
verify(notifyListenerCallback()).called(2); // verify listener were notified twice
});
test('unit tests are independent from each other', () {
exampleModel.increment();
expect(exampleModel.value, 1);
exampleModel.increment();
expect(notifyListenerCallback()).called(2); // verify listener were notified twice. This only works, if you have reset your mocks
});
});
}
Just keep in mind that if you trigger the same mock callback function in multiple tests you have to reset your mock callback function in the setup to reset its counter.
I've ran into the same Issue. It's difficult to test wether notifyListeners was called or not especially for async functions. So I took your Idea with the listenerCallCount and put it to one function you can use.
At first you need a ChangeNotifier:
class Foo extends ChangeNotifier{
int _i = 0;
int get i => _i;
Future<bool> increment2() async{
_i++;
notifyListeners();
_i++;
notifyListeners();
return true;
}
}
Then the function:
Future<R> expectNotifyListenerCalls<T extends ChangeNotifier, R>(
T notifier,
Future<R> Function() testFunction,
Function(T) testValue,
List<dynamic> matcherList) async {
int i = 0;
notifier.addListener(() {
expect(testValue(notifier), matcherList[i]);
i++;
});
final R result = await testFunction();
expect(i, matcherList.length);
return result;
}
Arguments:
The ChangeNotifier you want to test.
The function which should fire notifyListeners (just the reference to the function).
A function to the state you want to test after each notifyListeners.
A List of the expected values of the state you want to test after each notifyListeners (the order is important and the length has to equal the notifyListeners calls).
And this is how to test the ChangeNotifier:
test('should call notifyListeners', () async {
Foo foo = Foo();
expect(foo.i, 0);
bool result = await expectNotifyListenerCalls(
foo,
foo.increment2,
(Foo foo) => foo.i,
<dynamic>[isA<int>(), 2]);
expect(result, true);
});
I have wrap it to the function
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
dynamic checkNotifierCalled(
ChangeNotifier notifier,
Function() action, [
Matcher? matcher,
]) {
var isFired = false;
void setter() {
isFired = true;
notifier.removeListener(setter);
}
notifier.addListener(setter);
final result = action();
// if asynchronous
if (result is Future) {
return result.then((value) {
if (matcher != null) {
expect(value, matcher);
}
return isFired;
});
} else {
if (matcher != null) {
expect(result, matcher);
}
return isFired;
}
}
and call it by:
final isCalled = checkNotifierCalled(counter, () => counter.increment(), equals(2));
expect(isCalled, isTrue);

Chain multiple calls with same arguments to return different results

I'm in the process of writing a Flutter app with some extensive unit test coverage.
I'm using Mockito to mock my classes.
Coming from a Java (Android) world where I can use Mockito to chain calls to return different values on subsequent calls.
I would expect this to work.
import 'package:test/test.dart';
import 'package:mockito/mockito.dart';
void main() {
test("some string test", () {
StringProvider strProvider = MockStringProvider();
when(strProvider.randomStr()).thenReturn("hello");
when(strProvider.randomStr()).thenReturn("world");
expect(strProvider.randomStr(), "hello");
expect(strProvider.randomStr(), "world");
});
}
class StringProvider {
String randomStr() => "real implementation";
}
class MockStringProvider extends Mock implements StringProvider {}
However it throws:
Expected: 'hello'
Actual: 'world'
Which: is different.
The only working way I found that works is by keeping track myself.
void main() {
test("some string test", () {
StringProvider strProvider = MockStringProvider();
var invocations = 0;
when(strProvider.randomStr()).thenAnswer((_) {
var a = '';
if (invocations == 0) {
a = 'hello';
} else {
a = 'world';
}
invocations++;
return a;
});
expect(strProvider.randomStr(), "hello");
expect(strProvider.randomStr(), "world");
});
}
00:01 +1: All tests passed!
Is there a better way?
Use a list and return the answers with removeAt:
import 'package:test/test.dart';
import 'package:mockito/mockito.dart';
void main() {
test("some string test", () {
StringProvider strProvider = MockStringProvider();
var answers = ["hello", "world"];
when(strProvider.randomStr()).thenAnswer((_) => answers.removeAt(0));
expect(strProvider.randomStr(), "hello");
expect(strProvider.randomStr(), "world");
});
}
class StringProvider {
String randomStr() => "real implementation";
}
class MockStringProvider extends Mock implements StringProvider {}
You're not forced to call when in the start of the test:
StringProvider strProvider = MockStringProvider();
when(strProvider.randomStr()).thenReturn("hello");
expect(strProvider.randomStr(), "hello");
when(strProvider.randomStr()).thenReturn("world");
expect(strProvider.randomStr(), "world");
Mockito is dart has a different behavior. Subsequent calls override the value.
Option 1 - With a Stub
I'm not sure about mockito, but you could do it using argument matchers, and creating a stub class that overrides that method instead of using a mock, like:
class StrProviderStub implements StrProvider {
StrProviderStub(this.values);
List<String> values;
int _currentCall = 0;
#override
String randomStr() {
final value = values[_currentCall];
_currentCall++;
return value;
}
}
And then use that Stub instead of the mock on the test
Option 2 - Use an Extension with a closure (Preferred)
extension MultipleExpectations<T> on PostExpectation<T> {
void thenAnwserInOrder(List<T> bodies) {
final safeBody = Queue.of(bodies);
thenAnswer((realInvocation) => safeBody.removeFirst());
}
}
# Then... use it like:
when(mock).thenAnwserInOrder(['First call', 'second call']);
I opened this issue to see if this could be added to the framework itself https://github.com/dart-lang/mockito/issues/568

stubbing a function in a proxyquired object

I want to unit-test the following simplified module:
const Logger = require('logplease');
const logger = Logger.create('utils');
const tester = {
one: () => {
logger.log('called real one()');
tester.two();
},
two: () => {
logger.log('called real two()');
},
};
module.exports = {
one: tester.one,
two: tester.two
};
I'm replacing the external dependency logplease using Proxyquire, which works very well. However I need to stub two() because I want to unit-test one() while eliminating the side-effects two() produces when it runs in real code.
it.only('stubbing functions on the "proxyquired" object under test', function(done) {
const loggerStub = {
create: () => {
return { log: (msg) => { console.log('fake logger: ', msg); } };
}
};
let tester = proxyquire('../tester', { 'logplease': loggerStub });
let stub2 = sinon.stub(
tester,
'two',
() => {
console.log('called fake stub of two()');
}
);
tester.one();
console.log('call count 2: ', stub2.callCount);
done();
});
Output I get:
fake logger: called real one()
fake logger: called real two()
call count 2: 0
Output I expect:
fake logger: called real one()
called fake stub of two()
call count 2: 1
Why doesn't my stub function run?
Short answer:
const Logger = require('logplease');
const logger = Logger.create('utils');
const tester = {
one: () => {
logger.log('called real one()');
tester.two();
},
two: () => {
logger.log('called real two()');
},
};
module.exports = tester;
Explanation: scope
You exported one and two as:
module.exports = {
one: tester.one,
two: tester.two
};
In this case tester.one knows only about this function:
two: () => {
logger.log('called real two()');
}
and have no idea about stubbed two. So you have two versions of two, just try to invoke tester.two() inside test.