TDD course with AdonisJs - 3. Model factories & DB transactions

Published 9/9/2019

Welcome back! Let's get right into our second test, Deleting threads!

You can find all the changes we make throughout this post here: https://github.com/MZanggl/tdd-adonisjs/commit/95a52a79de271c126a3a1e0a8e087fb87d040555

Now in order to delete a thread, we first have to create a thread. For now let's just do this manually in the test, but in the end, we are going to refactor this again!

Add a new test inside thread.spec.js

test('can delete threads', async ({ assert, client }) => {
  const thread = await Thread.create({
    title: 'test title',
    body: 'test body',
  })

  const response = await client.delete(`threads/${thread.id}`).send().end()
  console.log(response.error)
  response.assertStatus(204)
})

Run it! We receive a 404, since we have not created the route yet, so let's add it to our resourceful route in routes.js. By convention the action to delete an entity is destroy.

// start/routes.js

Route.resource('threads', 'ThreadController').only(['store', 'destroy'])

We now get the error RuntimeException: E_UNDEFINED_METHOD: Method destroy missing, so let's create the method in our ThreadController.

async destroy({ params }) {
    const thread = await Thread.findOrFail(params.id)
    await thread.delete()
}

The test passes! But now let's make sure it was actually deleted from the database. Head over to the test and add the following check at the end of our test.

assert.equal(await Thread.getCount(), 0)

Whoops!

1. can delete threads
  expected 1 to equal 0
  1 => 0

How did that happen, we are deleting it right?

Let's try running only the "can delete threads" test and see what happens

npm t -- -g 'can delete threads'

or alternatively

adonis test -g 'can delete threads'

It passes, right?

It makes sense, since we never deleted the inserted thread from the first test. To fix this we simply have to load another trait at the top of the test.

trait('DatabaseTransactions')

This will wrap all queries in a transaction that gets rolled back after each test, so when our second test runs, the thread from the first test is long rolled back. Give the test suite a run!

Check out my e-book!

Learn to simplify day-to-day code and the balance between over- and under-engineering.

Refactoring

Okay, there is quite a lot to refactor in our test.

Let's first look at these lines

const thread = await Thread.create({
    title: 'test title',
    body: 'test body',
  })

The more tests we need, the more tedious this becomes. Luckily Adonis allows to create model factories. For this, head over to database/factory.js and add the following code.

Factory.blueprint('App/Models/Thread', (faker) => {
  return {
    title: faker.word(),
    body: faker.paragraph(),
  }
})

Also uncomment const Factory = use('Factory') at the top of the file.

faker is an instance of https://chancejs.com, check out their documentation for all the things you can fake.

Now back in our test we can replace the manual thread creation with simply

const thread = await Factory.model('App/Models/Thread').create()

Also, add const Factory = use('Factory') to the top of the test.

Run the tests and you should still get green!


There is also a nicer way of doing

const response = await client.delete(`threads/${thread.id}`).send().end()

In particular threads/${thread.id}. It would be more elegant if we were able to do const response = await client.delete(thread.url()).send().end(), in fact let's just do that and run the test. It will complain that thread.url is not a function.

For this to work, we have to add the method url to our threads model. But currently, we are inside an integration test. So how can we do this the TDD way?

The solution is to break down from the feature test into a unit test for our Thread model.

Let's create a test using adonis make:test Thread and this time choose unit.

This is what the unit test will look like

'use strict'

const { test, trait } = use('Test/Suite')('Thread')
const Factory = use('Factory')

trait('DatabaseTransactions')

test('can access url', async ({ assert }) => {
  const thread = await Factory.model('App/Models/Thread').create()
  assert.equal(thread.url(), `threads/${thread.id}`)
})

Nicely throwing the same error TypeError: thread.url is not a function. Remember how I said TDD follows the concept red -> green -> refactor. What I did not mention before, but what we just learned, is that these three steps are happening in a loop!

Head over to app/Models/Thread.js and add the following method to the Thread class

url() {
    return `threads/${this.id}`
}

Run the test again, this time both the unit and the functional test should be green!

Now we can already create and delete threads, but so far, even guests can perform these actions. Next time let's see how we can restrict these actions to only authenticated users and add a user_id field to our threads table.