I'm learning tests and using :
Express
Jest
SuperTest
Sequelize
This is user.test.js, Everytime I ran the test, it create a user object in database, in app.get("/"),I have a:
User.findAll({});
so I would like to test someting more "generic" like a User Object, its possible, what do you suggest me?
const app = require("../app");
const request = require("supertest");
const { User } = require("../models/");
const db = require("../models");
describe('User Model', () => {
beforeAll(async () => {
const user = await db.User.create({ name: "Paulo", email: "blablblalbalbal#gmail.com", password: "123mudar" });
return user
})
it('List User', async () => {
const res = await request(app).get("/");
expect(res.body).toBe(user);
expect(res.statusCode).toBe(200);
});
});
You should mock your calls to DB or anywhere outside of your testing area.
Related
I have a very basic Express.js app which I use Jest and Supertest to test. The routes are not set up until the database is connected:
class App {
public app: express.Application;
public mainRoutes: Util = new Util();
constructor() {
this.app = express();
AppDataSource.initialize()
.then(() => {
// add routes which rely on the database
this.mainRoutes.routes(this.app);
})
.catch((error) => console.log(error));
}
}
export default new App().app;
Here is my test:
describe("Util", function () {
test("should return pong object", async () => {
const res = await request(app).get("/ping");
expect(res.statusCode).toEqual(200);
expect(res.body).toEqual({ message: "pong" });
});
});
Since I put in the promise, this has been 404ing. I can't add async to the constructor. I tried refactoring the class to separate the connection with setting up the routes, but it didn't seem to help.
This works:
test("should return pong object", async () => {
setTimeout(async () => {
const res = await request(app).get("/ping");
expect(res.statusCode).toEqual(200);
expect(res.body).toEqual({ message: "pong" });
}, 1000);
});
But obviously I don't want to add a setTimeout. How is this usually done? I am new to testing.
Just remove the setTimeout() and await the call to the application. You should be initializing the application in the beforeAll() method, which I assume you have, to get the application up and running in the testing space. You should also mock your database connection, so you can fake the data you want back, and not have to wait for the external database to actually be available.
// Create a mock for your database, and have it return whatever you need
import <your-database-class> = require('database');
jest.mock('database', () => {
...
});
describe("Util", function () {
beforeAll(async () => {
app = await <whatever you do to launch your application>
});
test('should be defined', () => {
expect(app).toBeDefined();
});
test("should return pong object", async () => {
const res = await request(app).get("/ping");
expect(res.statusCode).toEqual(200);
expect(res.body).toEqual({ message: "pong" });
});
});
I'm trying to build a scraping script to get a bunch of Discord server's total members. I actually did that with Puppeteer like below but I think my IP address has been banned because I'm getting "Invite Invalid" error from Discord even though invite links are working.
My question is that does Discord have APIs to get any server's total member count? Or is there any 3rd party library for that purpose? Or any other method?
const puppeteer = require('puppeteer')
const discordMembers = async ({ server, browser }) => {
if (!server) return
let totalMembers
const page = await browser.newPage()
try {
await page.goto(`https://discord.com/invite/${server}`, {
timeout: 3000
})
const selector = '.pill-qMtBTq'
await page.waitForSelector(selector, {
timeout: 3000
})
const totalMembersContent = await page.evaluate(selector => {
return document.querySelectorAll(selector)[1].textContent
}, selector)
if (totalMembersContent) {
totalMembers = totalMembersContent
.replace(/ Members/, '')
.replace(/,/g, '')
totalMembers = parseInt(totalMembers)
}
} catch (err) {
console.log(err.message)
}
await page.close()
if (totalMembers) return totalMembers
}
const asyncForEach = async (array, callback) => {
for (let i = 0; i < array.length; i++) {
await callback(array[i], i, array)
}
}
const run = async () => {
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox']
})
const servers = ['tQp4pSE', '3P5K3dzgdB']
await asyncForEach(servers, async server => {
const members = await discordMembers({ server, browser })
console.log({ server, members })
// result
// { server: 'tQp4pSE', members: 57600 }
// { server: '3P5K3dzgdB', members: 159106 }
})
await browser.close()
}
run()
Update: Mar 22, 2022
Thanks for #Vaviloff's answer we can actually access Discord's private APIs but the problem is it's only accessible over browser. I'm getting Request failed with status code 400 issue from Axios. Is it a CORS issue? How do we get the results in a Node.js app?
const axios = require('axios')
const discordMembers = async ({ server }) => {
try {
const apiResult = await axios({
data: {},
method: 'get',
url: `https://discord.com/api/v9/invites/${server}?with_counts=true&with_expiration=true`
})
console.log(apiResult)
} catch (err) {
console.log(err)
}
}
discordMembers({ server: 'tQp4pSE' })
A lot of modern web applications have their own internal APIs. Oftentimes you can spot frontend making requests to it, by using Networking tab in Devtools (filter by Fetch/XHR type):
Such API endpoints can change any time of course, but usually the last for a long time and is a rather convenient way of scraping
Currently Discord uses this URL for basic instance description:
https://discord.com/api/v9/invites/tQp4pSE?with_counts=true&with_expiration=true
By accessing it you get the desired data:
Update
To make your code work don't send any data in the request:
const apiResult = await axios({
method: 'get',
url: `https://discord.com/api/v9/invites/${server}?with_counts=true&with_expiration=true`
})
I use Jest + SuperTest to test API. I would like to get the token and store it globally (using agent() method. It works fine when I have hardcoded token and use agent() to set it globally like this:
const supertest = require("supertest");
const baseUrl = "https://gorest.co.in/public/v1";
const request = supertest
.agent(baseUrl)
.set(
"Authorization",
">>>here comes hardcoded token value<<<"
);
describe("Posts endpoint", () => {
it.only("should be able to create a post", async () => {
const resp = await request.get("/users");
const user_id = resp.body.data[0].id;
const response = await request.post("/posts").send({
title: "foo",
body: "bar",
user_id: user_id,
});
expect(response.statusCode).toBe(201);
});
});
but I don't know how to get the token from auth endpoint and pass it there instead of this hardcoded one. Here is the function of getting token in beforeAll().
let token = "";
beforeAll(async () => {
const response = await request(baseUrl).post("/auth").send({
username: "test#example.com",
password: "password",
});
token = response.body.access_token;
});
Does anyone have any idea how to handle that with SuperTest?
I think the order of your code is just slightly wrong. Although some functions get hoisted when testing, it might be clearer to write the code the way it should execute.
You have the write idea, as the beforeAll() is executed before a beforeEach or other test. This problem is that you are creating your request in Supertest before you run the beforeAll and get the token.
const supertest = require("supertest");
const baseUrl = "https://gorest.co.in/public/v1";
describe("Posts endpoint", () => {
let token = "";
beforeAll(async () => {
const response = await request(baseUrl).post("/auth").send({
username: "test#example.com",
password: "password",
});
token = response.body.access_token;
});
let request;
beforeEach(async () => {
request = supertest
.agent(baseUrl)
.set('Authorization', token)
;
});
it.only("should be able to create a post", async () => {
const resp = await request.get("/users");
const user_id = resp.body.data[0].id;
const response = await request.post("/posts").send({
title: "foo",
body: "bar",
user_id: user_id,
});
expect(response.statusCode).toBe(201);
});
});
I have a documents router which has router.post('/mine', [auth, uploadFile], async (req, res) => { ... }) route handler. The actual implementation of this route handler is below.
documents.js router
const createError = require('./../helpers/createError');
const auth = require('./../middlewares/auth');
const uploadFile = require('./../middlewares/uploadFile');
const express = require('express');
const router = express.Router();
router.post('/mine', [auth, uploadFile], async (req, res) => {
try {
let user = await User.findById(req.user._id);
let leftDiskSpace = await user.leftDiskSpace();
if(leftDiskSpace < 0) {
await accessAndRemoveFile(req.file.path);
res.status(403).send(createError('Your plan\'s disk space is exceeded.', 403));
} else {
let document = new Document({
filename: req.file.filename,
path: `/uploads/${req.user.username}/${req.file.filename}`,
size: req.file.size
});
document = await document.save();
user.documents.push(document._id);
user = await user.save();
res.send(document);
}
} catch(ex) {
res.status(500).send(createError(ex.message, 500));
}
});
module.exports = router;
I'm currently writing integration tests using Jest and Supertest. My current documents.test.js test file is below:
documents.test.js test file
const request = require('supertest');
const { Document } = require('../../../models/document');
const { User } = require('../../../models/user');
const fs = require('fs');
const path = require('path');
let server;
describe('/api/documents', () => {
beforeEach(() => { server = require('../../../bin/www'); });
afterEach(async () => {
let pathToTestFolder = path.join(process.cwd(), config.get('diskStorage.destination'), 'user');
// Remove test uploads folder for next tests
await fs.promises.access(pathToTestFolder)
.then(() => fs.promises.rm(pathToTestFolder, { recursive: true }))
.catch((err) => { return; });
// Remove all users and documents written in test database
await User.deleteMany({});
await Document.deleteMany({});
server.close();
});
describe('POST /mine', () => {
it('should call user.leftDiskSpace method once', async () => {
let user = new User({
username: 'user',
password: '1234'
});
user = await user.save();
let token = user.generateAuthToken();
let file = path.join(process.cwd(), 'tests', 'integration', 'files', 'test.json');
let documentsRouter = require('../../../routes/documents');
let errorToThrow = new Error('An error occured...');
user.leftDiskSpace = jest.fn().mockRejectedValue(errorToThrow);
let mockReq = { user: user };
let mockRes = {};
documentsRouter.post = jest.fn();
documentsRouter.post.mockImplementation((path, callback) => {
if(path === '/mine') {
console.warn('called');
callback(mockReq, mockRes);
}
});
const res = await request(server)
.post('/api/documents/mine')
.set('x-auth-token', token)
.attach('document', file);
expect(documentsRouter.post).toHaveBeenCalled();
expect(user.leftDiskSpace).toHaveBeenCalled();
});
});
});
I create mock post router handler for documents.js router. As you can see from mockImplementation for this route handler, it checks if the path is equal to '/mine' (which is my supertest endpoint), then calls console.warn('called'); and callback. When I run this test file, I can not see any yellow warning message with body 'called'. And also when POST request endpoint /api/documents/mine the server doesn't trigger my mock function documentsRouter.post. It has never been called. So I think the server's documents router is not getting replaced with my mock post route handler. It still uses original post route handler to respond my POST request. What should I do to test if my mock documentsRouter.post function have been called?
Note that my User model has a custom method for checking left disk space of user. I also tried to mock that mongoose custom method but It also doesn't work.
I want to use sinon to write test code for the business logic of a service using ExpressJS and Mongoose.
I wrote the following test code, but findOneService takes only id as an argument and returns a document with that id.
//service_test.js
const sinon = require('sinon');
// Service
const { findOneService } = require('../services/service');
// Schema
const Post = require('../models/mongoose/schemas/post');
describe('findOneService', () => {
let find;
beforeEach(() => {
find = sinon.stub(Post, 'findOne');
});
afterEach(() => {
find.restore();
});
it('should findOne', async () => {
const id = ???;
...?
});
})
//service.js
exports.findOneDocument = async (id) => {
const result = await Post.findOne({_id: id});
if (!result) {
throw new Error('404');
}
return result;
};
How can I define the result of this to pass the test code?
To test this kind of behaviour, I strongly suggest an integration test (with an embedded/dockerized MongoDB, for example). This would allow to test-drive more things than just the service, such as schema, migration, db config.
However, if you just want to test-drive the if (!result)... logic, you can stick with sinon. What you're missing is stubbing the return value:
it('returns the document if found', async () => {
find.returns('a post');
expect(await findOneService.findOneDocument('id')).toReturn('a post');
});
it('throw error when document does not exist', async () => {
find.returns(null);
expect(() => await findOneService.findOneDocument('non-existent id')).toThrow(Error);
});