4
\$\begingroup\$

I wrote an express API for a document management service (repo to be found here) and even though I understand the importance of unit-testing I never quite managed to wrap my head around how I would test an API like this. I have no idea if my test are actually testing something useful or are covering everything that could be tested:

describe('User', () => {
before((done) => {
    user.deleteMany({}, (err) => {
        done()
    })
})

describe('/POST signup', () => {
    it('should not create a new user with invalid mail syntax', (done) => {
        let body = {
            username: 'test_user',
            password: 'test_password',
            mail: 'invalid',
            diplayName: 'Testi Test',
        }

        chai
            .request(server)
            .post('/user/signup')
            .send(body)
            .end((err, res) => {
                res.should.have.status(500)
                res.body.should.be.a('object')
                res.body.payload.should.have.a.property('message')
                res.body.payload.message.should.be.a('string')
                done()
            })
    })

    it('should create a new user with valid syntax', (done) => {
        let body = {
            username: 'test_user',
            password: 'test_password',
            mail: 'test@test.de',
            diplayName: 'Testi Test',
        }

        chai
            .request(server)
            .post('/user/signup')
            .send(body)
            .end((err, res) => {
                res.should.have.status(200)
                res.body.should.be.a('object')
                res.body.payload.should.have.a.property('user')
                res.body.payload.should.have.a.property('token')
                res.body.payload.user.should.be.a('object')
                res.body.payload.token.should.be.a('string')
                done()
            })
    })
})

describe('/POST login', () => {
    it('should not login without password or username', (done) => {
        let body = {
            username: 'test_user',
        }

        chai
            .request(server)
            .post('/user/login')
            .send(body)
            .end((err, res) => {
                res.should.have.status(401)
                res.body.should.be.a('object')
                res.body.payload.should.have.a.property('message')
                res.body.payload.message.should.be.a('string')
            })

        body = {
            password: 'test_password',
        }

        chai
            .request(server)
            .post('/user/login')
            .send(body)
            .end((err, res) => {
                res.should.have.status(401)
                res.body.should.be.a('object')
                res.body.payload.should.have.a.property('message')
                res.body.payload.message.should.be.a('string')
            })

        done()
    })

    it('should not login with wrong credentials', (done) => {
        body = { username: 'wrong_name', password: 'test_password' }

        chai
            .request(server)
            .post('/user/login')
            .send(body)
            .end((err, res) => {
                res.should.have.status(401)
                res.body.should.be.a('object')
                res.body.payload.should.have.a.property('message')
                res.body.payload.message.should.be.a('string')
                done()
            })
    })

    it('should login with correct credentials', (done) => {
        body = { username: 'test_user', password: 'test_password' }

        chai
            .request(server)
            .post('/user/login')
            .send(body)
            .end((err, res) => {
                res.should.have.status(200)
                res.body.should.be.a('object')
                res.body.payload.should.have.a.property('user')
                res.body.payload.should.have.a.property('token')
                res.body.payload.user.should.be.a('object')
                res.body.payload.token.should.be.a('string')
                done()
            })
    })
})
})

These are the routes to be tested:

router
    .route('/login')
    /**
     * @api {post} /user/login User login
     * @apiName userLogin
     * @apiGroup User
     * @apiDescription Logs user in and returns the user and API token
     * @apiParam {String} username
     * @apiParam {String} password
     * @apiSuccess {Object} user User profile
     * @apiSuccess {String} token API token
     * @apiError (401) {String} LoginFailed
     */
    .post(async (req, res) => {
        try {
            let result = await user.getAuthenticated(
                req.body.username,
                req.body.password
            )

            const token = await jwt.sign(result.toJSON(), process.env.JWT_SECRET, {
                expiresIn: process.env.JWT_EXPIRES,
            })

            res.status(200).json({ payload: { user: result, token: token } })
        } catch (error) {
            res.status(401).json({ payload: { message: 'Login failed' } })
        }
    })

router
    .route('/signup')
    /**
     * @api {post} /user/signup User signup
     * @apiName userSignup
     * @apiGroup User
     * @apiDescription Signs user up and logs in automatically
     * @apiParam {String} username Username
     * @apiParam {String} password Password according to policy
     * @apiParam {String} mail Valid email
     * @apiParam {String} displayName Full name
     * @apiSuccess {Object} user User profile
     * @apiSuccess {String} token API token
     * @apiError (500) {String} InternalError Something went wrong during signup. Most likely to be during validation.
     * @apiDeprecated Users should not be allowed to sign up by themselfes but rather be invited to use docSys
     */
    .post(async (req, res) => {
        try {
            let newUser = new user({
                username: req.body.username,
                password: req.body.password,
                mail: req.body.mail,
                settings: {
                    language: 'en',
                    displayName: req.body.displayName,
                },
            })

            const token = jwt.sign(newUser.toObject(), process.env.JWT_SECRET, {
                expiresIn: process.env.JWT_EXPIRES,
            })

            await newUser.save()

            res.status(200).json({ payload: { user: newUser, token: token } })
        } catch (error) {
            res.status(500).json({ payload: { message: error.message } })
        }
    })

My questions are:

  1. Are these tests sufficient to cover these routes?
  2. Am I testing the mongoose model in a seperate test or can I just assume that it's doing its thing correctly if tests like these pass?

I know it's quite stupid to write tests after big portions of the API have already been written, but I figured I'll have to start at some point.

\$\endgroup\$
  • \$\begingroup\$ "Are these tests sufficient to cover these routes?" Are you aware there are automated tools that test your coverage? You should still read up on the basics so you understand what you're actually doing, but it looks like you're asking us to verify the correctness of your tests. Did you try running them? Do you have any metrics on them? \$\endgroup\$ – Mast Apr 12 at 14:07
  • \$\begingroup\$ Thank for your comment. I'm more asking if this is the correct way of testing an API: specifying "use cases", because wouln't that just test the scenarios I could think of? I'm aware of test coverage and still trying to get into it. As said I'm more thinking of the amount of scenarios to test for each route. The tests are running great and are passing. \$\endgroup\$ – Felix Wieland Apr 12 at 15:57
1
\$\begingroup\$

First, I suggest separating the tests of each endpoint to a different file.

Signup tests

Do you have validations for other fields except for the email? If so add tests for them. Examples: duplicate email, duplicate user name, weak password.

You can also test what happens if the structure of the request is wrong. For example missing fields.

You can add more tests with different invalid emails according to the email validation logic. But I suggest testing this in unit tests.

Login tests

I think you should have the following tests:

  • Login without Signup
  • Login with a wrong username (after signup)
  • Login with a wrong password (after signup)
  • Successful login

You should have a setup of signup in those tests. Which makes me wonder how 'should login with correct credentials' works?

I saw in the code you have a mechanism for too many wrong login attempts so you should add tests for it.

Assertions

I see you are checking the status code and the structure of the payload but not its content.

Failures Tests

Since you are writing tests after the code is written, you can write tests for failures looking at the code and understand its assumptions. a simple example: what happens if the DB is down.


In this answer, I am Ignoring the effort of writing those tests. It is your job to decide which worth the effort and which not.

You said

I know it's quite stupid to write tests after big portions of the API have already been written

So I encourage you to try TDD for your next API endpoint :-)

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Thanks a lot for your comment, it already is very helpful to me! What exactyl do you mean by a setup of signup? \$\endgroup\$ – Felix Wieland Apr 16 at 16:22
  • \$\begingroup\$ @FelixWieland in order to test login you need to perform signup. All the operations in a test done before the tested function are called setup. \$\endgroup\$ – shanif Apr 16 at 16:41
  • \$\begingroup\$ Ah ok, shouldnt the order of my tests already satisfy that requirement? I'm creating a test user in my db that I am then again using to check the login function. The db is only cleared at the very beginning of my test run. \$\endgroup\$ – Felix Wieland Apr 16 at 18:55
  • 1
    \$\begingroup\$ @FelixWieland you shouldn't make test depand on other test executing. Each test should be able to run alone. Also when those tests run in the build you want to be able to run them in parallel. \$\endgroup\$ – shanif Apr 16 at 19:10
  • \$\begingroup\$ I'll add to shanif's comment – when your db can be fully torn down and setup with fixtures before each test, it's a beautiful thing. Your logical units of code are truly independent and can perform their single responsibility. \$\endgroup\$ – Charlie Schliesser Apr 24 at 16:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.