I've been reading some articles, and posts here on Stack Overflow about when I should mock a function and when I shouldn't, but I have a case where I'm not sure about what to do.
I have a UserService class which uses dependency injection concept to receive dependencies through its constructor.
class UserService {
constructor(userRepository) {
this.userRepository = userRepository;
}
async getUserByEmail(userEmail) {
// would perform some validations to check if the value is an e-mail
const user = await this.userRepository.findByEmail(email);
return user;
}
async createUser(userData) {
const isEmailInUse = await this.getUserByEmail(userData.email);
if(isEmailInUse) {
return "error";
}
const user = await this.userRepository.create(userData);
return user;
}
}
I want to test if the createUser method works properly, and for my tests, I created a fake userRepository which is basically a object with mocked methods that I will use while instantiating UserService Class
const UserService = require('./UserService.js');
describe("User Service tests", () => {
let userService;
let userRepository;
beforeEach(() => {
userRepository = {
findOne: jest.fn(),
create: jest.fn(),
}
userService = new UserService(userRepository);
});
afterEach(() => {
resetAllMocks();
});
describe("createUser", () => {
it("should be able to create a new user", async () => {
const newUserData = { name: 'User', email: 'user#test.com.br' }
const user = { id: 1, name: 'User', email: 'user#test.com.br' }
userRepository.create.mockResolvedValue(user);
const result = await userService.createUser();
expect(result).toStrictEqual(user);
})
})
})
Note that in the createUser method, there is a call to the getUserByEmail method which is also a method of UserService class, and that is where I got confused.
Should I mock the getUserByEmail method even it is a method of the class I'm testing? If it is not the correct approach, what should I do?
You should almost always prefer not to mock parts of the thing you're supposed to be testing, in this case UserService. To illustrate why, consider these two tests:
Provides a test double implementation for findByEmail on the repo object:
it("throws an error if the user already exists", async () => {
const email = "foo#bar.baz";
const user = { email, name: "Foo Barrington" };
const service = new UserService({
findByEmail: (_email) => Promise.resolve(_email === email ? user : null),
});
await expect(service.createUser(user)).rejects.toThrow("User already exists");
});
Stubs out the service's own getUserByEmail method:
it("throws an error if the user already exists", async () => {
const email = "foo#bar.baz";
const user = { email, name: "Foo Barrington" };
const service = new UserService({});
service.getUserByEmail = (_email) => Promise.resolve(_email === email ? user : null);
await expect(service.createUser(user)).rejects.toThrow("User already exists");
});
For your current implementation, both pass just fine. But let's think about how things might change.
Imagine we need to enrich the user model getUserByEmail provides at some point:
async getUserByEmail(userEmail) {
const user = await this.userRepository.findByEmail(userEmail);
user.moreStuff = await.this.userRepository.getSomething(user.id);
return user;
}
Obviously we don't need this extra data just to know whether or not the user exists, so we factor out the basic user object retrieval:
async getUserByEmail(userEmail) {
const user = await this._getUser(userEmail);
user.moreStuff = await.this.userRepository.getSomething(user.id);
return user;
}
async createUser(userData) {
if (await this._getUser(userData.email)) {
throw new Error("User already exists");
}
return this.userRepository.create(userData);
}
async _getUser(userEmail) {
return this.userRepository.findByEmail(userEmail);
}
If we were using test 1, we wouldn't have to change it at all - we're still consuming findByEmail on the repo, the fact that the internal implementation has changed is opaque to our test. But with test 2, that's now failing even though the code still does the same things. This is a false negative; the functionality works but the test fails.
In fact you could have applied that refactor, extracting _getUser, prior to a new feature making the need so clear; the fact that createUser uses getUserByEmail directly reflects accidental duplication of this.userRepository.findByEmail(email) - they have different reasons to change.
Or imagine we make some change that breaks getUserByEmail. Let's simulate a problem with the enrichment, for example:
async getUserByEmail(userEmail) {
const user = await this.userRepository.findByEmail(userEmail);
throw new Error("lol whoops!");
return user;
}
If we're using test 1, our test for createUser fails too, but that's the correct outcome! The implementation is broken, a user cannot be created. With test 2 we have a false positive; the test passes but the functionality doesn't work.
In this case, you could say that it's better to see that only getUserByEmail is failing because that's where the problem is, but I'd contend that would be pretty confusing when you looked at the code: "createUser calls that method too, but the tests say it's fine...".
You shouldn't mock any of these functions since its creating users and reading data from the database. If you mock them then what's the point of the test. In other words, you wouldn't know if your app is working correctly with the database or not. Anyway, I would mock functions such as the functions that send emails and so on. Don't mock the functions that are the heart of the application. You should have a database for testing and another one for production.
Related
I have a query for getting projects from the DB which has args as shown below for filtering projects. How would I use the same args in the #resolveField for filtering form values of the project
#Query(() => [Project])
async getProjects(#Args() projectArgs: ProjectArgs) {
return await this.projectsService.find(projectArgs);
}
#ResolveField("formValues", () => [FormValues])
async getFormValues(#Parent() project: Project) {
const { id } = project;
return await this.formsService.findValues({ projectId: id});
}
I have faced this problem before and through thorough searching, I have come to the conclusion that the best and scalable way of doing this is to define the args for the field resolver itself. Here is how you do it
#ResolveField("formValues", () => [FormValues])
async getFormValues(#Args() projectArgs: ProjectArgs) {
..../////
}
This way you will have to pass the args just as you pass it in the parent query. Or you could leave them out if you don't want to filter the form values
There are other approaches that you can use. One of them is by setting the info as
#Query(() => [Project])
async getProjects(#Args() projectArgs: ProjectArgs, #Info() info) {
info.variableValues.some_key = value
return await this.projectsService.find(projectArgs);
}
But this won't scale and make it very tightly coupled
I'm struggling to see why some code is behaving as it is.
Vue3 front end login page interacting with Google Firebase services (authentication and firestore db)
const onSubmit = () => {
.
.
.
// above missing section just gathers some form data
signInUser(form_email.value, form_passowrd.value)
.then(() => {
const user = firebase.auth().currentUser
if (user.emailVerified) {
checkUser(user.uid)
router.push({ name: 'Profile' })
}
else {
resendVerification.value = true
commandNotification('show', 'error', 'Unverified Account')
}
})
.catch((error) => {
if (error.code === 'auth/invalid-email') {
commandNotification('show', 'error', 'Invalid Email Address')
}
// followed by other checks
})
}
signInUser is the official firebase auth email and password sign in function that is imported into this module.
So, user is signed in, then I check to see if the user has verified their email address. If they have I do some further checks in a "users" firebase table, and then route user to their profile page.
The problem that I am having is that the router.push is happening before the check user function has completed. I've even put a 10 second setTimeout between the two lines and the router push comes immediately.
It seems checkUser is running asynchronously but I didn't think it was. I've not marked it async myself.
The checkUser function itself does the following
const checkUser = (userid) => {
db.collection("users").doc(userid).get()
.then((d) => {
if (d.exists) {
userStateStore.action(MutationType.user.updateUser,
{
id: d.data().id,
country: d.data().country,
handle: d.data().handle
}
}
else {
// user creation when they do not exist
}
})
}
I'm currently testing with a user that already exists. Because the route is being called immediately the profile data is not in the vuex store.
I'm looking for some pointers on how to approach debugging this, or an answer if you see a specific problem.
Many thanks in advance.
checkUser is async. It should return the promise that get() returns...
const checkUser = (userid) => {
return db.collection("users").doc(userid).get()
// ...
I'd cleanup the caller (without async/await) like this...
const onSubmit = () => {
return signInUser(form_email.value, form_passowrd.value)
.then(() => {
const user = firebase.auth().currentUser
return user.emailVerified ? checkUser(user.uid) : Promise.resolve(false)
}).then(checkUserResult => {
if (checkUserResult) {
router.push({ name: 'Profile' });
} else {
resendVerification.value = true
commandNotification('show', 'error', 'Unverified Account')
}
})
// ...
These could also be made tidier (and exactly equivalent) using async/await syntax.
It seems checkUser is running asynchronously but I didn't think it was. I've not marked it async myself.
Yes, .get() is asynchronous and even though you haven't used async/await you're instead using .then().
One solution could be to put the router.push() invocation within the callback of the .then() within checkUser().
I have a Firebase cloud function that does this:
const admin = require('firebase-admin')
admin.initializeApp()
exports.setSessionState = functions.https.onCall(async (data, context) => {
const stateId = data.stateId
const details = data.details
const stateRef = admin.database().ref(`state/${stateId}`)
stateRef.transaction((state) => {
if (state) {
state.details = details
}
return state
})
})
The code works well in the actual application and the state is updated but I run into problems when running this in a test, where the state is always null. (In other words, the details are never set.) The test uses the Mocha framework and is run against a real Firebase project and I can't see how anything would differ here but the behaviour is consistently different when calling this cloud function from the client application and when calling it from the test suite:
const chai = require('chai')
const assert = chai.assert
const test = require('firebase-functions-test')({
databaseURL: '<redacted>',
storageBucket: '<redacted>',
projectId: '<redacted>',
}, '../service-account-credentials.json')
describe('CFsetSessionState', () => {
let wrapped
let cloudFunctions
let admin
before(() => {
cloudFunctions = require('../index')
admin = require('firebase-admin')
wrapped = test.wrap(cloudFunctions.CFsetSessionState)
})
it('Test setting state', async () => {
const stateId = 'abc'
const data = {
stateId: stateId,
details: {
name: 'New name'
}
}
const context = {
auth: {
uid: 123
}
}
const stateRef = admin.database().ref(`state/${stateId}`)
await stateRef.set({
name: 'Previous name',
active: true
})
await wrapped(data, context)
const snapshot = await stateRef.once('value')
const state = snapshot.val()
// Details do not exist here, why?
assert.equal(state.details.name, 'New name')
})
})
I leave the database state as is after the tests so I can see there is indeed data in the state object but the details have not been set. Experimenting a bit with setting and fetching data (using .once()) before and after the call to the cloud function can change the behaviour, making me think it might be some cache issue, but this experimenting does not give me any particular stable end state. I have no idea how the equivalent of a local cache works in cloud functions, it currently shows random behaviour.
What could cause this?
I haven't tried running your Cloud Function with your test, but most probably the problem comes from the fact that you incorrectly manage the life cycle of your Cloud Function. Since it is Callable one, you need to terminate it by returning a Promise (or a value when all the asynchronous work is completed). More details here and here in the doc.
You should therefore adapt your CF as follows:
exports.setSessionState = functions.https.onCall(async (data, context) => {
const stateId = data.stateId
const details = data.details
const stateRef = admin.database().ref(`state/${stateId}`)
return stateRef.transaction((state) => { // Note the return here
if (state) {
state.details = details
}
return state
})
})
We are returning the Promise returned by the Transaction.
I would like to have a AuthWrapper Service that wraps the AngularFireAuth Service. Something like this.
// EDIT: Adding some import statements.
import {TestBed} from '#angular/core/testing';
import { AuthWrapperService } from './auth-wrapper.service';
import {AngularFireModule} from '#angular/fire';
import {AngularFireAuth, AngularFireAuthModule} from '#angular/fire/auth';
import {environment} from '../environments/environment';
#Injectable({
providedIn: 'root'
})
export class AuthWrapper {
constructor(public afAuth: AngularFireAuth) { }
isAuthenticated(): Observable<firebase.User> {
return this.afAuth.user;
}
createUserWithEmailAndPassword(email: string, password: string): Promise<String> {
let authPromise: Promise<firebase.auth.UserCredential> =
this.afAuth.auth.createUserWithEmailAndPassword(email, password);
return authPromise
.then((value) => {
return value.user.email;
})
.catch(function(error) {
return error.message;
});
}
}
I want a wrapper, so that I can test my connection to AngularFireAuth. Other tests mock the AuthWrapper.
( Reason for not mocking AngularFireAuth: Say I mock AngularFireAuth, then I am determining the mock's return values. Then I am saying that I understand what these values would look like. It is not safe to assume this without ever testing these by calling the backend. Say google changes how the results of the real AngularFireAuth's methods, I would then be forced to change the results of each of my AngularFireAuth mocks. Instead it is better to wrap AngularFireAuth in a wrapper, and just change that wrapper's methods to conform to google's changes. )
My tests in Karmine and Jasmine result in an "Async callback was not invoked within 5000ms error." I know the user is signed it because the first expect passes, but how do I get the second expect to work?:
describe('AuthWrapperService', () => {
let fireAuthService: AngularFireAuth;
let password = "dcbA4321!";
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
AngularFireModule.initializeApp(environment.firebase),
AngularFireModule,
AngularFireAuthModule
],
providers: [AngularFireAuth, AuthWrapperService],
declarations: []
})
fireAuthService = TestBed.get(AngularFireAuth);
});
it('should sign in user', async() => {
const email = "anyemail#gmail.com";
let authWrap = new AuthWrapperService(fireAuthService);
let userEmail = await authWrap.createUserWithEmailAndPassword(email, password);
expect(userEmail).toBe("anyemail#gmail.com");
let obs = authWrap.isAuthenticated();
let promise = obs.toPromise();
let user = await promise.then((result) => {return result});
expect(user.email).toBe("anyemail#gmail.com");
});
});
I haven't seen any Angular testing code, where the AngularFireAuth isn't mocked.
These are the tests that worked for me with a time interval of 5000ms. I did test them with "bad#gmail.com" in the expect lines to make sure the expect lines were actually running, and yes, the expect lines are actually passing. I intend to only run the AuthWrapperService tests periodically since they hit the database. I will mock the AuthWrapperService in other tests. Before I run these tests I delete the existing users in the firebase database manually through the firebase console. I do run both of these tests in one go, (inside one describe block).
it('should create new user', async() => {
const annEmail = "ann#gmail.com";
let authWrap = new AuthWrapperService(fireAuthService);
await authWrap.createUserWithEmailAndPassword(annEmail, password).then(
((testEmail) => {
expect(fireAuthService.auth.currentUser.email).toBe(annEmail);
})
)
await fireAuthService.auth.signOut();
});
it('should return error message when user already exists', async() => {
const email = 'ben#gmail.com';
const error = 'already in use';
let authWrap = new AuthWrapperService(fireAuthService);
await authWrap.createUserWithEmailAndPassword(email, password)
.then((value: String) => {
expect(fireAuthService.auth.currentUser.email).toBe(email);
});
await fireAuthService.auth.signOut();
expect(fireAuthService.auth.currentUser).toBeNull();
await authWrap.createUserWithEmailAndPassword(email, password)
.then((value: String) => {
expect(value).toContain('already in use');
});
await fireAuthService.auth.signOut();
});
I decided to use fireAuthService.auth.currentUser.email instead of the AuthWrapperService's isAuthenticated() method to directly test that my AuthWrapperService's createUserWithEmailAndPassword() method creates a user in firebase.
I realize that this is not a unit test, but I have decided that it is better to have an integration test than to mock methods that I don't own.
The following test also worked for me within 5000ms and uses the isAuthenticated() method.
it('should create user cat', async() => {
const catEmail = "cat#gmail.com";
let authWrap = new AuthWrapperService(fireAuthService);
let userEmail = await authWrap.createUserWithEmailAndPassword(catEmail, password);
expect(userEmail).toBe(catEmail);
await authWrap.isAuthenticated().subscribe({
next(user) {
expect(user.email).toBe(catEmail);
}
});
console.log("cat, timeinterval: " + jasmine.DEFAULT_TIMEOUT_INTERVAL);
await fireAuthService.auth.signOut();
});
The following "dan test" resulted in time outs. I tried 29000ms once and it ran out of time. Most runs of this test were 10 seconds. Many times, but not always, this test passes correctly if the user is already a user before the test is run (so it's not really creating a user during the test). One time this test passed correctly when the user had not been created before the test, but I ran this many times without the user existing before the test and it failed. I'm not sure why that would be.
it('should create user dan', done => {
const datEmail = "dan#gmail.com";
let authWrap = new AuthWrapperService(fireAuthService);
authWrap.isAuthenticated().subscribe(
{
next(user){
expect(user.email).toBe(datEmail);
done();
}
}
);
authWrap.createUserWithEmailAndPassword(datEmail, password);
console.log("dan, timeinterval: " + jasmine.DEFAULT_TIMEOUT_INTERVAL);
});
As discussed in comments, its not a good practice to not mock dependencies in case of unit testing. Any cohesive unit should be tested in isolation. And let say if our dependency has number of other dependencies then we cannot provide them all.
When you test the code with the actual dependency, you are not doing unit testing; you are doing integration testing.
Please see this for difference between Integration testing and Unit testing - https://angular.io/guide/testing#use-e2e-end-to-end-to-test-more-than-a-single-unit
You are facing this error because your async spec finishes after the default time out that jasmine has specified, which is 5 seconds. Please find the jasmine documentation on this. URL - https://jasmine.github.io/2.0/introduction.html#section-42
You need to manipulate the jasmine.DEFAULT_TIMEOUT_INTERVAL as per your need, assign it some large value as shown in below example:
describe("long asynchronous specs", function() {
var originalTimeout;
beforeEach(function() {
originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000; // 60 second (use value as per need)
});
it("takes a long time", function(done) {
setTimeout(function() {
done();
}, 9000);
});
afterEach(function() {
jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
});
});
It still depend on the network as well how fast your promise returns the value. I would suggest you to change the value for jasmine.DEFAULT_TIMEOUT_INTERVAL only in test where you are working with actual dependecies.
I hope this will help.
I'm using NestJS CQRS recipe in order to manage interactions between two entities: User and UserProfile. The architecture is an API Gateway NestJS server + a NestJS server for each microservice (User, UserProfile, etc.).
I have already set up basic interactions through User and UserProfile modules on API Gateway with their own sagas/events/commands:
When a user is created, a user profile is created
When the user profile creation fails, the previously created user is deleted
In details:
In User module, CreateUser command raises a UserCreated event that is intercepted by User saga, which will trigger CreateUserProfile command (from UserProfile module).
If the latter fails, a UserProfileFailedToCreate event is raised and intercepted by UserProfile saga, which will trigger DeleteUser command (from User module).
Everything works fine.
If the CreateUser command fails, I resolve(Promise.reject(new HttpException(error, error.status)) which indicates to the end user that something went wrong during the user creation.
My problem is that I cannot replicate the same behavior for the CreateUserProfile command since the HTTP request promise has already been resolved from the first command, obviously.
So my question is: is there any way to make a command fail if a subsequent command fails in the saga? I understand that the HTTP request is totally disconnected from any subsequent commands triggered by a saga, but I want to know if anybody has already played with events or something else here to replicate this data flow?
One of the reasons I'm using CQRS, besides having a much cleaner code for data interactions among microservices, is to be able to rollback repositories actions in case any of the chained commands fails, which works fine. But I need a way to indicate to the end user that the chain went through an issue and was rollbacked.
UserController.ts
#Post('createUser')
async createUser(#Body() createUserDto: CreateUserDto): Promise<{user: IAuthUser, token: string}> {
const { authUser } = await this.authService.createAuthUser(createUserDto);
// this is executed after resolve() in CreateUserCommand
return {user: authUser, token: this.authService.createAccessTokenFromUser(authUser)};
}
UserService.ts
async createAuthUser(createUserDto: CreateUserDto): Promise<{authUser: IAuthUser}> {
return await this.commandBus
.execute(new CreateAuthUserCommand(createUserDto))
.catch(error => { throw new HttpException(error, error.status); });
}
CreateUserCommand.ts
async execute(command: CreateAuthUserCommand, resolve: (value?) => void) {
const { createUserDto } = command;
const createAuthUserDto: CreateAuthUserDto = {
email: createUserDto.email,
password: createUserDto.password,
phoneNumber: createUserDto.phoneNumber,
};
try {
const user = this.publisher.mergeObjectContext(
await this.client
.send<IAuthUser>({ cmd: 'createAuthUser' }, createAuthUserDto)
.toPromise()
.then((dbUser: IAuthUser) => {
const {password, passwordConfirm, ...publicUser} = Object.assign(dbUser, createUserDto);
return new AuthUser(publicUser);
}),
);
user.notifyCreated();
user.commit();
resolve(user); // <== This makes the HTTP request return its reponse
} catch (error) {
resolve(Promise.reject(error));
}
}
UserSagas.ts
authUserCreated = (event$: EventObservable<any>): Observable<ICommand> => {
return event$
.ofType(AuthUserCreatedEvent)
.pipe(
map(event => {
const createUserProfileDto: CreateUserProfileDto = {
avatarUrl: '',
firstName: event.authUser.firstName,
lastName: event.authUser.lastName,
nationality: '',
userId: event.authUser.id,
username: event.authUser.username,
};
return new CreateUserProfileCommand(createUserProfileDto);
}),
);
}
CreateUserProfileCommand.ts
async execute(command: CreateUserProfileCommand, resolve: (value?) => void) {
const { createUserProfileDto } = command;
try {
const userProfile = this.publisher.mergeObjectContext(
await this.client
.send<IUserProfile>({ cmd: 'createUserProfile' }, createUserProfileDto)
.toPromise()
.then((dbUserProfile: IUserProfile) => new UserProfile(dbUserProfile)),
);
userProfile.notifyCreated();
userProfile.commit();
resolve(userProfile);
} catch (error) {
const userProfile = this.publisher.mergeObjectContext(new UserProfile({id: createUserProfileDto.userId} as IUserProfile));
userProfile.notifyFailedToCreate();
userProfile.commit();
resolve(Promise.reject(new HttpException(error, 500)).catch(() => {}));
}
}
UserProfileSagas.ts
userProfileFailedToCreate = (event$: EventObservable<any>): Observable<ICommand> => {
return event$
.ofType(UserProfileFailedToCreateEvent)
.pipe(
map(event => {
return new DeleteAuthUserCommand(event.userProfile);
}),
);
}
DeleteUserCommand.ts
async execute(command: DeleteAuthUserCommand, resolve: (value?) => void) {
const { deleteAuthUserDto } = command;
try {
const user = this.publisher.mergeObjectContext(
await this.client
.send<IAuthUser>({ cmd: 'deleteAuthUser' }, deleteAuthUserDto)
.toPromise()
.then(() => new AuthUser({} as IAuthUser)),
);
user.notifyDeleted();
user.commit();
resolve(user);
} catch (error) {
resolve(Promise.reject(new HttpException(error, error.status)).catch(() => {}));
}
}
In DDD terms your creation of User and UserProfile constitutes a business transaction - a group of business operations/rules that must be consistent - that spans multiple microservices.
In that case returning the database User before the UserProfile has been created means you return data in an inconsistent state. This is not necessarily wrong, but you should handle this appropriately in the client if you do it like that.
I see three possible ways to deal with this scenario:
You let the Sagas run until they have executed commands that indicate the business transaction has ended, only then resolve an outcome for the client indicating either success or failure (e.g. in error details you can report which steps succeeded and which did not). So you do not yet resolve in CreateAuthUserCommand.
If it can take a long time for the UserProfile to be created (it could even have to be manually validated by a Moderator) then you might want to resolve the User in CreateAuthUserCommand and then subsequently have the client subscribe to UserProfile-related events. You need a mechanism for that, but it decouples the client from the running transaction, and it can do other stuff.
Alternatively you could break up the business transaction into two parts for which the client sends separate requests: one creates/returns the authenticated User, and the other returns the created UserProfile. Though it seems that User + UserProfile belong to the same bounded context, the fact that they reside in two different microservices may indicate they are not (in this case I think the first microservice really is for Authentication and the other for UserProfiles which indicate different bounded contexts to me). Best-practice is to have a microservice implement their own encapsulated bounded context.
(Note: Answered an old question in hopes it is informative to others)