Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add docs for endpoint testing with mongoose #1420

Merged
merged 15 commits into from
Sep 2, 2017

Conversation

zellwk
Copy link
Contributor

@zellwk zellwk commented Jun 23, 2017

Docs for endpoint tests. #849.

Let me know if I'm opening the PR correctly?

Signed-off-by: Zell Liew <zellwk@gmail.com>
Signed-off-by: Zell Liew <zellwk@gmail.com>
Signed-off-by: Zell Liew <zellwk@gmail.com>
Signed-off-by: Zell Liew <zellwk@gmail.com>
Copy link

@OmgImAlexis OmgImAlexis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since all other recipes use semicolons at the end of lines this should too.

**Next, start your MongoDB instance with Mongomem**

```js
test.before('start server', async t => await MongoDBServer.start())

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The await is redundant here since it's in a return statement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

const res = await request(app)
.get('/test1')
.send({
email: 'someemail@gmail.com'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might want to switch this to example@example.com just to be safe that you're not using a real email in an example.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@OmgImAlexis
Copy link

OmgImAlexis commented Jun 25, 2017

Hopefully this is just a copying error but while testing this in my own project all I manage to get is an error thrown and my tests all fail.

➜  api.wvvw.me git:(master) ✗ yarn ava -- test/user.spec.js  --verbose
yarn ava v0.24.6
$ "/Users/xo/code/api.wvvw.me/node_modules/.bin/ava" test/user.spec.js --verbose

  ✖ start server Rejected promise returned by test

  1 test failed [12:36:40]

  start server

  Rejected promise returned by test

  Rejection reason:

    "Mongod shutting down"


error Command failed with exit code 1.

@novemberborn novemberborn changed the title Add docs for endpont testing with mongoose Add docs for endpoint testing with mongoose Jun 25, 2017
@novemberborn
Copy link
Member

@zellwk we already have a recipe that uses mongomem and mongoose, written by the creator of mongomem. Rather than adding a new recipe, how do we improve the current one?

Signed-off-by: Zell Liew <zellwk@gmail.com>
@zellwk
Copy link
Contributor Author

zellwk commented Jun 26, 2017

@OmgImAlexis Hmm. Would you mind trying again? Recipe looks right to me. Whats in your user.spec.js

@novemberborn I can edit the file directly, would that work better? Regarding that recipe, I tested it and it doesn't work directly with my app because the Mongoose instances are different, which is why I decided to make this recipe.

@OmgImAlexis
Copy link

OmgImAlexis commented Jun 26, 2017

Here's my ./user.spec.js. My ./main.js exports app as default.

import test from 'ava';
import request from 'supertest';
import {MongoDBServer} from 'mongomem';

import {setupFixtures, removeFixtures, setupMongoose} from './utils';

test.before('start server', async () => {
    await MongoDBServer.start();
});

test.beforeEach(async t => {
    const db = await setupMongoose();
    const app = require('../main');

    // Setup any fixtures you need here. This is a placeholder code
    await setupFixtures();

    // Pass app and mongoose into your tests
    t.context.app = app;
    t.context.db = db;
});

test.afterEach.always(async t => {
    const {db} = t.context;
    // Note: removeFixtures is a placeholder. Write your own
    await removeFixtures();
    await db.connection.close();
});

// Note the serial tests
test.serial('create', async t => {
    const {app} = t.context;
    const res = await request(app).post('/user').send({
        username: 'ava',
        password: 'avarocks'
    });
    t.is(res.status, 201);
    t.is(res.body.username, 'ava');
});

test.after.always('cleanup', () => MongoDBServer.tearDown());

./utils/index.js

import mongoose from 'mongoose';
import {MongoDBServer} from 'mongomem';

const setupFixtures = () => {};
const removeFixtures = () => {};
const setupMongoose = async () => {
    await mongoose.connect(await MongoDBServer.getConnectionString());
    return mongoose;
};

export {
    setupFixtures,
    removeFixtures,
    setupMongoose
};

@zellwk
Copy link
Contributor Author

zellwk commented Jun 26, 2017

Looks right. Can you tell me how you handled /user?

@OmgImAlexis
Copy link

In ./main.js I import all my routes and then use app.use('/user', user);

./routes/user.js

import {Router} from 'express';
import HTTPError from 'http-errors';

import log from '../log';
import config from '../config';
import {User} from '../models';
import {isValidObjectId} from '../middleware';

const router = new Router();

router.get(['/', '/:id'], isValidObjectId, async (req, res, next) => {
    if (req.params.id) {
        const user = await User.find({
            _id: req.params.id
        }).exec().catch(next);

        return res.send(user);
    }

    const users = await User.find({}).sort({
        date: -1
    }).exec().catch(next);

    res.send(users);
});

router.post('/', (req, res, next) => {
    if (!config.get('signups.enabled')) {
        // @TODO: This may not be the best status code for this.
        return next(new HTTPError.MethodNotAllowed('Signups are currently disabled.'));
    }

    const user = new User({
        username: req.body.username || '',
        email: req.body.email || '',
        password: req.body.password
    });
    log.debug(user);
    user.save().then(() => {
        log.debug('User saved.');
        return res.send({
            user
        });
    }).catch(err => {
        log.debug('User error.');
        // Duplicate field
        if (err.code === 11000) {
            return next(new HTTPError.Conflict('User already exists.'));
        }
        return next(err);
    });
});

export default router;

@zellwk
Copy link
Contributor Author

zellwk commented Jun 26, 2017

Do you mind trying out a handler like this?

app.get('/user', (req, res) => {
  const { email } = req.body
  User.findOne({email})
   .then(r => res.json(r))
  .catch(e => /* handle whatever errors */)
})

@OmgImAlexis
Copy link

Okay this is an issue with mongomem, looks like it's trying to bind to 0.0.0.0:27017 even though it's using get-port.

Ref: CImrie/mongomem#2

@zellwk
Copy link
Contributor Author

zellwk commented Jun 26, 2017

Ah yes. I had the same problem too. Just to confirm. This recipe works fine for you. Yeah?

@OmgImAlexis
Copy link

Yep after releasing the port from my docker container it's now fine.

@OmgImAlexis
Copy link

As per CImrie/mongomem#2 (comment) maybe this should use mongodb-memory-server instead since ava needs to run all tests in parallel?

@novemberborn
Copy link
Member

@novemberborn I can edit the file directly, would that work better? Regarding that recipe, I tested it and it doesn't work directly with my app because the Mongoose instances are different, which is why I decided to make this recipe.

Both recipes are very similar to each other. Ideally we have one. If there's some additional use case or nuance we can discuss it in the recipe.

@zellwk
Copy link
Contributor Author

zellwk commented Jul 2, 2017

@OmgImAlexis I have tried using mondob-memory-server with the following code, but was unable to get tests to run in parallel still.

test.beforeEach(async t => {
  const mongod = new MongodbMemoryServer()
  const uri = await mongod.getConnectionString()
  await mongoose.connect(uri)
  await setupFixtures()
  const app = require('../../server')

  t.context.mongod = mongod
  t.context.app = app
  t.context.db = mongoose
})

test.afterEach.always(async t => {
  const { db, mongod } = t.context
  await removeFixtures()
  await db.connection.close()
  await mongod.stop()
})

The error I get when attempting to run tests in parallel is this:

litmus › beforeEach for litmus post test
  /Users/zellwk/Projects/private-repos/course-server/node_modules/mongoose/lib/connection.js:211

  Rejected promise returned by test

  Rejection reason:

    [Error: Trying to open unclosed connection.]

  NativeConnection.Connection._handleOpenArgs (node_modules/mongoose/lib/connection.js:211:11)
  NativeConnection.Connection.open (node_modules/mongoose/lib/connection.js:306:37)
  Mongoose.connect (node_modules/mongoose/lib/index.js:259:47)
  Test.<anonymous> (test/routes/litmus.spec.js:17:18)
  step (test/routes/litmus.spec.js:25:191)

Here, it seems like, without using mongooseInstance = new mongoose.Mongoose() as this recipe suggests, we can't test databases in parallel. But if we do use new mongoose.Mongoose(), we have to change how mongoose connects within the app. (And I have no idea how to do it). If you have any suggestions, I'd be happy to try!

@zellwk
Copy link
Contributor Author

zellwk commented Jul 2, 2017

Both recipes are very similar to each other. Ideally we have one. If there's some additional use case or nuance we can discuss it in the recipe.

@novemberborn Sure. We can merge it into one recipe eventually. Can we iron out the issues mentioned above before doing so? That way, we can see if there's really an additional use case or nuance.

@OmgImAlexis
Copy link

@nodkz since you're the dev for mongodb-memory-server would you be able to help here?

}

test.beforeEach(async t => {
const db = await setupMongoose()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be better to inline the setupMongoose function here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure


test.beforeEach(async t => {
const db = await setupMongoose()
const app = require('./server')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't reloaded for every test. Better to require it outside the test hook.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'require' statement configures Mongoose according to the current test (I believe). May not work properly if Mongoose is not set up per test.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modules are cached after the first time they're loaded, so requiring ./server for each test shouldn't make a difference.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right about this. Let me make the requested changes after confirming if there's a better implementation with @nodkz, as commented below.

const app = require('./server')

// Setup any fixtures you need here. This is a placeholder code
await setupFixtures()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems pretty critical actually. How does app use the db instance?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will expand on this!

const { db } = t.context
// Note: removeFixtures is a placeholder. Write your own
await removeFixtures()
await db.connection.close()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another way is to disconnect mongoose. Works the same, as far as I can tell. Awaiting @nodkz's input on mongodb-memory-server right now. Otherwise, tests can't run in parallel. Should we wait? Or should I edit according to your comments first?

@nodkz
Copy link

nodkz commented Jul 11, 2017

@OmgImAlexis @zellwk
Sorry guys for the late response.

You may test mongoose in several ways:

  • As described in README for JEST example
  • Or something complex via mocking mongoose module. Like it done in graphql-compose-mongoose:
    • mock mongoose just mock connect method and adds callback on disconnect for shutdowning mongo server.
    • user model used mocked mongoose module to define schema/model
    • test1 - beforeAll(() => UserModel.base.connect()); calls only once mocked connect method per test file, and afterAll(() => UserModel.base.disconnect()); for disconnecting mongoose and shutdowning server and freeing event loop for correct test exit
    • test2 - same operations.

This tests works in parallel. Each test file has own mongodb server.


Also it should work for your case, where you use a dedicated database for every test case in one test file. If not, please provide test repo i try to figure out what is the problem.


I see following disadvantages in your example:

  • low performance it's not good to use for every test case dedicated server. It's ok to use one server per one test file. If you need for every test case different db:
  • also it may be some internal mongoose problems if AVA runs test in event loop, not in separate processes. Cause mongoose package use globals inside and it may produce some errors when different test cases set different connection strings and tries to connect.
  • mongomem in parallel tests will download 70MB files first time for every test (it's bad for CI envs). mongodb-memory-server has internal lock file for such cases and should download binaries only once. Also you may provide required version of mongo server via new MongodbMemoryServer({ binary: { version: '3.4.4' }}); mongomem does not allow you to choose mongod version.

@zellwk
Copy link
Contributor Author

zellwk commented Jul 13, 2017

@nodkz Thanks for chipping in! I've tried your suggestions, but I still couldn't get ava tests to run in parallel. By parallel, I mean tests within a test file. So far, my test file looks like this:

import test from 'ava'
import app from '../server'
import request from 'supertest'
import User from '../models/User'
import mongoose from '../handlers/mongoose'
import MongodbMemoryServer from 'mongodb-memory-server'

const mongod = new MongodbMemoryServer()

test.before(async t => {
  mongoose.connect(await mongod.getConnectionString())
})

test.beforeEach(async t => {
  const user = new User({email: 'one@example.com', name: 'One'})
  const user2 = new User({email: 'two@example.com', name: 'Two'})
  const user3 = new User({email: 'three@example.com', name: 'Three'})
  await user.save()
  await user2.save()
  await user3.save()
  t.context.app = app
})

test.afterEach.always(async t => {
  await User.remove()
})

test('litmus get test', async t => {
  const { app } = t.context
  const res = await request(app)
    .get('/litmus')
    .send({email: 'one@example.com'})
  t.is(res.status, 200)
  t.is(res.body.name, 'One')
})

// Works if we use test.serial instead of test
test('litmus post test', async t => {
  const { app } = t.context
  const res = await request(app)
    .post('/litmus')
    .send({
      email: 'new@example.com',
      name: 'New'
    })
  t.is(res.status, 200)
  t.is(res.body.name, 'New')
  t.is(200, 200)
})

test.after.always(async t => {
  mongoose.disconnect()
  mongod.stop()
})

Is this the best we can do? If it helps with debugging, you can find a demo repo at https://github.com/zellwk/ava-mdb-test.

@zellwk
Copy link
Contributor Author

zellwk commented Jul 21, 2017

@nodkz, @novemberborn, @OmgImAlexis any suggestions on the demo repo I've sent? If not, I'll go ahead and edit this docs again.

@nodkz
Copy link

nodkz commented Jul 21, 2017

@zellwk miss your prev message. Now I'm looking your repo. Write about results today later.

@nodkz
Copy link

nodkz commented Jul 21, 2017

Tests do not work on node 7.2.1

Got following error:

$ ava --watch
/Users/nod/www/_sandbox/ava-mdb-test/server.js:16
app.get('/litmus', async (req, res) => {
                         ^
SyntaxError: Unexpected token (
    at Object.exports.runInThisContext (vm.js:78:16)
    at Module._compile (module.js:543:28)
    at loader (/Users/nod/www/_sandbox/ava-mdb-test/node_modules/babel-register/lib/node.js:144:5)
    at require.extensions.(anonymous function) (/Users/nod/www/_sandbox/ava-mdb-test/node_modules/babel-register/lib/node.js:154:7)
    at extensions.(anonymous function) (/Users/nod/www/_sandbox/ava-mdb-test/node_modules/require-precompiled/index.js:16:3)
    at Object.require.extensions.(anonymous function) [as .js] (/Users/nod/www/_sandbox/ava-mdb-test/node_modules/ava/lib/process-adapter.js:100:4)
    at Module.load (module.js:488:32)
    at tryModuleLoad (module.js:447:12)

    at Function.Module._load (module.js:439:3)
    at Module.require (module.js:498:17)
    at require (internal/module.js:20:19)
    at Object.<anonymous> (/Users/nod/www/_sandbox/ava-mdb-test/test/litmus.spec.js:2:1)
    at Module._compile (module.js:571:32)
    at extensions.(anonymous function) (/Users/nod/www/_sandbox/ava-mdb-test/node_modules/require-precompiled/index.js:13:11)
    at Object.require.extensions.(anonymous function) [as .js] (/Users/nod/www/_sandbox/ava-mdb-test/node_modules/ava/lib/process-adapter.js:100:4)
    at Module.load (module.js:488:32)
    at tryModuleLoad (module.js:447:12)
    at Function.Module._load (module.js:439:3)
    at Module.require (module.js:498:17)
    at require (internal/module.js:20:19)
    at Object.<anonymous> (/Users/nod/www/_sandbox/ava-mdb-test/node_modules/ava/lib/test-worker.js:61:1)
    at Module._compile (module.js:571:32)
    at Object.Module._extensions..js (module.js:580:10)
    at Module.load (module.js:488:32)
    at tryModuleLoad (module.js:447:12)
    at Function.Module._load (module.js:439:3)
    at Module.runMain (module.js:605:10)
    at run (bootstrap_node.js:420:7)
    at startup (bootstrap_node.js:139:9)
    at bootstrap_node.js:535:3

On 8.2.0 works perfectly. I think that NOTE is required in docs about minimal node version.

Fix mongoose deprecationWarning: open()

Following console warning:

(node:5017) DeprecationWarning: `open()` is deprecated in mongoose >= 4.11.0, use `openUri()` instead, or set the `useMongoClient` option if using `connect()`
 or `createConnection()`. See http://mongoosejs.com/docs/connections.html#use-mongo-client

Was fixed in the beggining of this week (needs to install the last mongoose version 4.11.3). Also it's required small config changes:

// handlers/mongoose.js
- mongoose.connect = async (uri) => {
+ mongoose.connect = async (uri, opts) => {
-   originalConnect.bind(mongoose)(uri)
+   originalConnect.bind(mongoose)(uri, opts)

// test/litmus.spec.js
- mongoose.connect(await mongod.getConnectionString())
+ mongoose.connect(await mongod.getConnectionString(), { useMongoClient: true })

For more info you may read docs and may see this issues #5423, huge #5399

Parallel tests inside one file do not work with one mongoose model. And never will not work.

Tests cases inside one file when runs in parallel uses the same proccess (checked it with console.log(process.pid)). So it means that all test cases work via EventLoop and on top of context used THE SAME mongoose module (THE SAME mongoose model USER). So in current setup it is impossible to test mongoose in parallel with different connections. Connection will be one and the same. So you got a dubplicate key error 11000.

How to solve it:

  1. DO NOT USE parallel tests inside one file [RECOMMENDED]. You always may split your tests in several files for which ava will use different process for every file (tested with your repo and it works perfectly). So you will not meet with mongoose globals restrictions.

  2. Create one more mock for User model. If you want to ran in parallel one mongoose schema with different connections you must create two mongoose models:

import mongoose from 'mongoose';
import MongodbMemoryServer from 'mongodb-memory-server';

mongoose.Promise = Promise;
const mongoServer = new MongodbMemoryServer();

const connections = {
  conn1: mongoose.createConnection(),
  conn2: mongoose.createConnection(),
  ...
  ...
};

const mongooseOpts = {
  server: {
    promiseLibrary = Promise;
    auto_reconnect: true,
    reconnectTries: Number.MAX_VALUE,
    reconnectInterval: 1000,
  },
};

mongoServer1.getConnectionString('server1_db1').then((mongoUri) => {
  connections.conn1.open(mongoUri, mongooseOpts);
  connection.once('open', () => {
    console.log(`MongoDB successfully connected to ${mongoUri}`);
  });
});

mongoServer1.getConnectionString('server1_db2').then((mongoUri) => {
  connections.conn2.open(mongoUri, mongooseOpts);
  connection.once('open', () => {
    console.log(`MongoDB successfully connected to ${mongoUri}`);
  });
});

...
...

export default connections;


// somewhere in other file
import { Schema } from 'mongoose';
import { conn1, conn2 } from './file_above';

const userSchema = new Schema({
  name: String,
});

export default {
  UserForConnection1: conn1.model('user', userSchema),
  UserForConnection2: conn2.model('user', userSchema),
  ...
}

It can be simplifed. But stays still complex for test. You should somehow mock User that for first test case it got UserForConnection1, and for second test case UserForConnection2, for third test ... and so one. I'm crying.

So my decision: DO NOT USE PARALLEL TESTS INSIDE ONE FILE FOR ONE MONGOOSE MODEL. You may split huge test into several files.

@nodkz
Copy link

nodkz commented Aug 17, 2017

In mongodb-memory-server@1.4.1 ship some changes with babel-polyfill.

Now your tests may work without babel-polyfill and regenerator.

typegoose/mongodb-memory-server#9 (comment)

@novemberborn
Copy link
Member

I'm not sure what to do with /docs/recipes/isolated-mongodb-integration-tests.md@master though. Again it's more focused on just MongoDB testing, with a bit of Mongoose added on. It uses a different memory server though. Perhaps it'd be sufficient for these recipes to reference each other.

I've tried the recipe before creating this one. The mongoose version doesn't work. Had a tough time understanding everything, until I hacked stuff together with @nodkz and @OmgImAlexis help in this PR.

So, I recommend removing the Mongoose parts, but keep the MongoDB parts for that recipe.

That's a good idea. Could you add that to this PR? I can fix it up afterwards to if you'd like.

@novemberborn
Copy link
Member

In mongodb-memory-server@1.4.1 ship some changes with babel-polyfill.

Now your tests may work without babel-polyfill and regenerator.

typegoose/mongodb-memory-server#9 (comment)

Thanks @nodkz! @zellwk could you check that's all good with this recipe, so we don't need the polyfill and stuff?

Signed-off-by: Zell Liew <zellwk@gmail.com>
Signed-off-by: Zell Liew <zellwk@gmail.com>
@zellwk
Copy link
Contributor Author

zellwk commented Aug 23, 2017

Thanks @nodkz! @zellwk could you check that's all good with this recipe, so we don't need the polyfill and stuff?

Done! babel-polyfill is no longer required. Woot!

So, I recommend removing the Mongoose parts, but keep the MongoDB parts for that recipe.

That's a good idea. Could you add that to this PR? I can fix it up afterwards to if you'd like.

Done too! Removed the Mongoose parts and pointed them to the mongoose docs instead. I'm not sure whether we should change Mongmem to Mongodb-memory server on that recipe. Thoughts, @nodkz?

@OmgImAlexis and @novemberborn, I also added an explanation on why the user should use test.serial instead of test with Mongoose. LMK if it looks good.

@@ -156,3 +139,13 @@ And you're done!
You may choose to abstract code for `test.before`, `test.beforeEach`, `test.afterEach.always` and `test.after.always` into a separate file.

To see a demo of this configuration file, look at https://github.com/zellwk/ava-mdb-test

## Why `test.serial` instead of `test`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just use a new database for each test? That's how we run tests with agenda and we don't have any issues.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you use mongoose, you interact with the database through Mongoose. Since there can only be one Mongoose instance (and that Mongoose instance must always point to the same database), we cant access multiple databases even if we create them.

Do I make sense?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! I understood @nodkz's explanation (above) wrongly. Although you can create multiple connections with Mongoose, you need to create multiple models. Let me switch the explanation as soon as I find some time to.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See User and UserOnServer2 models constructed on one schema with different connections in the following example:
https://github.com/nodkz/mongodb-memory-server#several-mongoose-connections-simultaneously

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@OmgImAlexis updated with a slightly better explanation. Do I make sense in this new one?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That does make sense but since ava is meant to be run in parallel it'd be better to explain how to set it up by default instead of expecting tests to run in serial.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fully understand that AVA runs tests in parallel, and your desire to set it up by default. I would love to do that too.

However, based on @nodkz's explanation, and my personal research over the past month, I strongly believe endpoint testing with Mongoose and AVA is an exception where serial tests would be preferred.

The setup for parallel tests changes how a person would create their schemas, and thus their app, too much for the additional benefit of running tests in parallel within each file. Besides, if you create multiple AVA test files, each test file still runs in parallel to other files, which shouldn't slow down testing too much.

I'll be glad to change the docs if you could show me a good way to run endpoint testing in parallel with Mongoose and Supertest.

@nodkz
Copy link

nodkz commented Aug 23, 2017

Firstly I'm using mongomem too, but when met with its downsides was writtenmongodb-memory-server.
mongomem downsides:

  • does not have lock for downloading binary (so when you start in parallel several tests from scratch, bins also will be downloaded several times).
  • does not allow to choose mongodb version and bunch of other options.

Full list of options for mongodb-memory-server:

const mongod = new MongodbMemoryServer({
  instance?: {
    port?: ?number, // by default choose any free port
    dbPath?: string, // by default create in temp directory
    storageEngine?: string, // by default `ephemeralForTest`
    debug?: boolean, // by default false
  },
  binary?: {
    version?: string, // by default '3.4.4'
    downloadDir?: string, // by default %HOME/.mongodb-prebuilt
    platform?: string, // by default os.platform()
    arch?: string, // by default os.arch()
    http?: any, // see mongodb-download package
    debug?: boolean, // by default false
  },
  debug?: boolean, // by default false
  autoStart?: boolean, // by default true
});

@zellwk
Copy link
Contributor Author

zellwk commented Aug 23, 2017

In that case, shall we continue with Mongomem with the mongodb recipe?

zellwk and others added 2 commits August 24, 2017 08:32
Signed-off-by: Zell Liew <zellwk@gmail.com>
* Consistent spelling of Express, Mongoose and SuperTest
* Move prerequisites into their own section, so the test file section
  reads better
* Reformat code examples
* Remove code comments from examples where they repeat the preceding
paragraph
* Reword paragraphs so they read better (in my humble opinion, that is)

I've also changed the section on using `test.serial`: I'm pretty sure
Mongoose already uses the same connection in the tests and the app.
Notably, the connection is made in a `test.before()` hook, not a
`test.beforeEach()` hook. Thus the real concern is tests interleaving
and the database being in an unknown or conflicting state.
@novemberborn
Copy link
Member

Thanks @zellwk et al. I think this is pretty much there! I did push some edits, so please let me know if I've butchered anything 😇

I've also changed the section on using test.serial: I'm pretty sure Mongoose already uses the same connection in the tests and the app. Notably, the connection is made in a test.before() hook, not a test.beforeEach() hook. Thus the real concern is tests interleaving and the database being in an unknown or conflicting state.

Please let me know if I got this wrong.

In that case, shall we continue with Mongomem with the mongodb recipe?

We can modify that recipe to use mongodb-memory-server in a follow-up PR.


```js
// Disconnect MongoDB and Mongoose after all tests are done
test.after.always(async t => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can just be () => { since t isn't used and there's nothing using async.


```js
// Cleans up database after every test
test.afterEach.always(async t => await User.remove())
test.afterEach.always(async t => await User.remove());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for the t argument here.

I'd change it to test.afterEach.always(() => User.remove());

When you run your first test, MongoDB downloads the latest MongoDB Binaries. It may take a minute. (The download is ~70mb).

**Add fixtures for each test**
When you run your first test, MongoDB downloads the latest MongoDB binaries. The download is ~70MB so this may take a minute.

You'll want to populate your database with dummy data. Here's an example:

```js
test.beforeEach(async t => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for the t argument.

// Create connection to mongoose before all tests
test.before(async t => mongoose.connect(await mongod.getConnectionString(), { useMongoClient: true }))
// Create connection to Mongoose before tests are run
test.before(async t => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for the t argument.

3. [Mongoose](http://mongoosejs.com)
1. [`mongod-memory-server`](https://github.com/nodkz/mongodb-memory-server) (A MongoDB in-memory Server)
2. [SuperTest](https://github.com/visionmedia/supertest) (An endpoint testing library)
3. [Mongoose ODM](http://mongoosejs.com)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this changed when "Mongoose" is know as "Mongoose" not "Mongoose ODM"?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I took that from the document title, but they don't refer to it that way elsewhere. Will change back, thanks!

@novemberborn
Copy link
Member

@OmgImAlexis updated.

@@ -76,7 +76,7 @@ test.beforeEach(async t => {
Dummy data should be cleared after each test:

```js
test.afterEach.always(async t => await User.remove());
test.afterEach.always(async () => await User.remove());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

async and await are redundant here since you're returning a Promise from User.remove().

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The explicitness doesn't hurt I think.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need for it and anyone using xo will need to remove it if they copy+paste. Better to just remove it and set an example by only shipping code that's linted even if it's only in the docs.

@zellwk
Copy link
Contributor Author

zellwk commented Aug 29, 2017

I've also changed the section on using test.serial: I'm pretty sure Mongoose already uses the same connection in the tests and the app. Notably, the connection is made in a test.before() hook, not a test.beforeEach() hook. Thus the real concern is tests interleaving and the database being in an unknown or conflicting state.

@novemberborn LGTM 👍

@novemberborn novemberborn merged commit c9fe8db into avajs:master Sep 2, 2017
@novemberborn
Copy link
Member

Thank you so much for your efforts @zellwk, @OmgImAlexis and @nodkz!

@cyberwombat
Copy link

Hi all. I've written a module to handle parallel testing with Mongo. It's in early test but please check it out. https://github.com/cyberwombat/mongoprime

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants