Question

I have written this function which checks if a user already exists in the application:

async function ValidateUserExists(username, email){
    if(!username || !email) throw new Error('Invalid number of args passed. Please pass username and email');
    let taken_valid_username = null;
    let taken_valid_email = null;
    if(username){
        taken_valid_username = await UsernameExists(username);
    }
    if(email){
        taken_valid_email = await EmailExists(email);
    }

    if(taken_valid_username) return taken_valid_username;
    if(taken_valid_email) return taken_valid_email;

    return null;
}

I have integration tests that prove UsernameExists() and EmailExists() work correctly.

It's main purpose is to see if a user already exists in the database. I can't just mock something here because both UsernameExists() and EmailExists() are designed to reach into the database and see if a user actually does exist.

I have a lot of functions like this as I'm building an app whose main purpose is to manipulate a database, meaning I have A LOT of integration testing happening.

So far the test I've written looks like this:

it('should see if a user already exists on an existent email', async ()=>{
    const test = await CreateDummyUser();
    const user = await ValidateUserExists(test.username, test.email);
    //expect user properties here
    await DestroyDummyUser(test);
})

Is there any approach to mocking/stubbing anyone has taken to attack composite functions like this? Is it sufficient enough that the functions is composed of pass?

Was it helpful?

Solution

In many cases, the answer is to change your design so that you can pass the capabilities you need as arguments to a new function, and then test the new function with "test doubles" replacing the things that are hard.

async function ValidateUserExists(username, email){
    return ValidateUserExistsV2(username, email, UsernameExists, ValidateUserExists);
}

async function ValidateUserExistsV2(username, email, usernameExists, validateUserExists) {
    if(!username || !email) throw new Error('Invalid number of args passed. Please pass username and email');

    let taken_valid_username = null;
    let taken_valid_email = null;

    if(username){
        taken_valid_username = await usernameExists(username);
    }
    if(email){
        taken_valid_email = await emailExists(email);
    }

    if(taken_valid_username) return taken_valid_username;
    if(taken_valid_email) return taken_valid_email;

    return null;
}

ValidateUserExistsV2 is easy to test, because in your test(s) you can define trivial implementations for usernameExists and validateUserExists; these implementations provide behaviors that mimic calls to a database.

Testing of the old function drops out of your unit test suite; most of the risk is taken care of by the testing of the new function. ValidateUserExists might count as "simple enough that we can test it with code review, but even if that isn't an option, it's certainly simple enough that we can mitigate the risks with fewer tests.

OTHER TIPS

You need to only mock the database connection underneath your functions.

If you are using dependency injection (or even a service locator) you should be able to swap the database connection out for a mocked one quite easily.

Licensed under: CC-BY-SA with attribution
scroll top