Create a Task Manager with Test Driven Development Part 4- Tasks
With a basic user setup, time to move on to the tasks. We’ll start with basic tasks that have a title, description, and completed field.
Let’s model the data first and then we can import it to write some tests. In a new task.js file in the models folder
const mongoose = require('mongoose');const taskSchema = mongoose.Schema(
{
title: {
type: String,
required: true,
trim: true,
},
description: {
type: String,
trim: true,
},
completed: {
type: Boolean,
default: false,
},
},
{ timestamps: true }
);const Task = mongoose.model('Task', taskSchema);module.exports = Task;
Nothing fancy here. I’ll also set the completed default value to false. For tests, let’s just see if we can add a task to the database and make sure we can’t add one without a title.
const request = require('supertest');
const mongoose = require('mongoose');
const app = require('../src/app');
const Task = require('../src/models/task');const taskOne = {
_id: new mongoose.Types.ObjectId(),
title: 'task one',
};const taskTwo = {
_id: new mongoose.Types.ObjectId(),
title: 'task two',
description: 'this task is set to complete',
completed: true,
};const setUpDatabase = async () => {
await Task.deleteMany();
await new Task(taskOne).save();
await new Task(taskTwo).save();
};describe('Adding task', () => {
beforeEach(setUpDatabase);
it('Should add a task', async () => {
const newTask = {
_id: new mongoose.Types.ObjectId(),
title: 'new task',
description: 'this task is new',
};
await request(app)
.post('/task')
.send(newTask)
.expect(201);
const task = await Task.findById(newTask._id);
expect(task).not.toBeNull();
});it("Should not add a task that doesn't have a title", async () => {
const id = new mongoose.Types.ObjectId();
await request(app)
.post('/task')
.send({
_id: id,
description: "This task doesn't have a title",
})
.send()
.expect(400); const task = await Task.findById(id);
expect(task).toBeNull();
});
});
Our first route is just like the add user one (in router/task.js).
const express = require('express');
const router = express.Router();
const Task = require('../models/task');router.post('/task', async (req, res) => {
const task = new Task(req.body);
try {
await task.save();
res.status(201).send(tconst taskRouter = require('./routers/task');
app.use(taskRouter);ask);
} catch (error) {
res.status(400).send();
}
});module.exports = router;
And we just need to make sure we register and user the router in our app.js file.
const taskRouter = require('./routers/task');
app.use(taskRouter);
And that’s it! We can now add tasks. Let’s create tests to read a task with valid and invalid ids.
describe('Read tasks', () => {
beforeEach(setUpDatabase);
it('Should read task by id', async () => {
const response = await request(app)
.get(`/task/${taskOne._id}`)
.send()
.expect(200);
expect(response.body).toMatchObject({ title: taskOne.title });
});it('Should return 404 for invalid id', async () => {
const id = new mongoose.Types.ObjectId();
await request(app)
.get(`/task/${id}`)
.send()
.expect(404);
});
});
Just like before, we’re going to use toMatchObject to see if the task that was returned is the same as the one we requested.
router.get('/task/:id', async (req, res) => {
try {
const task = await Task.findById(req.params.id);
if (!task) {
return res.status(404).send();
}
res.send(task);
} catch (error) {
res.status(500).send();
}
});
With this we have the reading tests passing as well.
For updating, we should be able to update everything except the id. So let’s write some tests for that next.
describe('Updating Tasks', () => {
beforeEach(setUpDatabase);it('Should update title, description, completed', async () => {
await request(app)
.patch(`/task/${taskTwo._id}`)
.send({ title: 'updated title',
description: 'updated description',
completed: false })
.expect(200);const task = await Task.findById(taskTwo._id);
expect(task.title).not.toBe(taskTwo.title);
expect(task.description).not.toBe(taskTwo.description);
expect(task.completed).not.toBe(taskTwo.completed);
});it('Should not update task if id field is specified', async () => {
await request(app)
.patch(`/task/${taskTwo._id}`)
.send({
_id: new mongoose.Types.ObjectId(),
title: 'updated title',
description: 'updated description',
completed: false,
})
.expect(400);
});it('Should return 404 for invalid id', async () => {
const id = new mongoose.Types.ObjectId();
await request(app)
.patch(`/task/${id}`)
.send(newTask)
.expect(404);
});
});
For our route,
router.patch('/task/:id', async (req, res) => {
const allowedUpdates = ['title', 'description', 'completed'];
const updates = Object.keys(req.body);
const isValidUpdate = updates.every(update => allowedUpdates.includes(update));
const task = await Task.findById(req.params.id); if (!task) {
return res.status(404).send();
} else if (!isValidUpdate) {
return res.status(400).send();
} try {
updates.forEach(update => (task[update] = req.body[update]));
await task.save();
res.send(task);
} catch (error) {
res.status(500).send();
}
});
First we’re checking if a valid id was sent, then if a valid update request was sent, and then saving the updated task. And our tests are passing.
Last up is delete.
describe('Delete task', () => {
beforeEach(setUpDatabase);
it('Should delete a task by ID', async () => {
await request(app)
.delete(`/task/${taskOne._id}`)
.send()
.expect(200);
const task = await Task.findById(taskOne._id);
expect(task).toBeNull();
});it('Should return 404 for invalid id', async () => {
const id = new mongoose.Types.ObjectId();
await request(app)
.delete(`/task/${id}`)
.send()
.expect(404);
});
});
For this one, I’ll just copy my delete route for the user and update it.
router.delete('/task/:id', async (req, res) => {
const id = req.params.id;
try {
const task = await Task.findByIdAndDelete(id);
if (!task) {
return res.status(404).send();
}
res.send(task);
} catch (error) {
res.status(400).send();
}
});
I could have done that for most of these tasks as well, but I wanted a bit of practicing writing this from scratch. And with that, we’re done with our basic tasks. After doing the user router, the task app was easy since it’s a very similar process. Obviously we still need to connect users to tasks, maybe create teams, and add a bunch of other features. But before working out more of the logic of the backend, I’m actually going to move to the front end next time and work on developing a very basic UI to interact with this project.