DEV Community

Cover image for TDD course with AdonisJs - 2. Our first test
Michael Z
Michael Z

Posted on • Updated on • Originally published at michaelzanggl.com

TDD course with AdonisJs - 2. Our first test

Originally posted at michaelzanggl.com. Subscribe to my newsletter to never miss out on new content.

You can find all the changes from this blog post here: https://github.com/MZanggl/tdd-adonisjs/commit/87bcda4823c556c7717a31ad977457050684bbcf

Let's start by creating our first real test. We focus on the central piece our app provides, threads. If you think about it, in order to create threads, we need a user to create threads, for that we need to implement registration and authentication. You might think that by that logic, registration and authentication should be the first thing we implement. However, user registration and authentication are not the central pieces of our application, so we don't have to care about these parts for now. Instead, let's start with a feature. (Protip: do the same when designing UI, no need to create the navbar and footer at first)

The first test is the hardest, since it requires some additional setup on the way, like setting up the database connection.

Let's create a test to create threads, we can easily do that from the command line:

adonis make:test Thread
Enter fullscreen mode Exit fullscreen mode

and select functional.

You can replace the content of the newly created file with the following

'use strict'

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

trait('Test/ApiClient')

test('can create threads', async ({ client }) => {

})

Enter fullscreen mode Exit fullscreen mode

We are loading the "apiClient" trait, which will provide us with the client variable we use for api requests to test our endpoints.

Okay, let's put some logic into the test. We keep it simple for now, posting to the threads endpoint with a title and body should return a response code of 200. Fair enough.

test('can create threads', async ({ client }) => {
  const response = await client.post('/threads').send({
    title: 'test title',
    body: 'body',
  }).end()

  response.assertStatus(200)
})
Enter fullscreen mode Exit fullscreen mode

Let's run the test suite to see what's happening.

The error we get is

1. can create threads
  expected 404 to equal 200
  404 => 200
Enter fullscreen mode Exit fullscreen mode

Of course! After all, we haven't created any route or controller yet. Still, we run the test to let it guide us what the next step is. What is so great about this approach is that it stops us from overengineering things. We do the bare minimum to get the test to pass. And once the tests are green, we refactor.

So let's head over to start/routes.js and add the following route

Route.post('threads', 'ThreadController.store')
Enter fullscreen mode Exit fullscreen mode

You might be inclined to add a route group or use resource routes at this point, but again, keep it simple, as simple as possible. We can refactor to something that scales better once the tests for this are green.

Running the test again will now return a different error!

  1. can create threads
  expected 500 to equal 200
  500 => 200
Enter fullscreen mode Exit fullscreen mode

We can log the response error in our test to see what's going wrong. For something more robust you could extend the exception handler.

// ...

console.log(response.error)
response.assertStatus(200)
Enter fullscreen mode Exit fullscreen mode

Now we know for sure the error is

'Error: Cannot find module 'app/Controllers/Http/ThreadController'
Enter fullscreen mode Exit fullscreen mode

So that's our next step!

Create the controller using adonis make:controller ThreadController and choose for HTTP requests.

Run the test and the error changes to RuntimeException: E_UNDEFINED_METHOD: Method store missing on ....

So let's create the "store" method on the controller and just make it return an empty object for now.

'use strict'

class ThreadController {
    async store({ response }) {
        return response.json({ })
    }
}

module.exports = ThreadController
Enter fullscreen mode Exit fullscreen mode

Running the test suite again will now make the test pass!

But obviously we are not quite done yet. So let's extend our test to confirm that we actually save threads into the database.

First, let's import the Thread model at the top of our test file.

const Thread = use('App/Models/Thread')
Enter fullscreen mode Exit fullscreen mode

Yes yes, this file does not yet exist, but we will just assume it does and let the test lead the way for the next step.

And in the test we will fetch the first thread from the database and assert that it matches the JSON response.

test('can create threads', async ({ client }) => {
  const response = await client.post('/threads').send({
    title: 'test title',
    body: 'body',
  }).end()
  console.log(response.error)
  response.assertStatus(200)
  const thread = await Thread.firstOrFail()
  response.assertJSON({ thread: thread.toJSON() })
})
Enter fullscreen mode Exit fullscreen mode

Running the test returns the error Error: Cannot find module 'app/Models/Thread'. So let's create it!

adonis make:model Thread -m
Enter fullscreen mode Exit fullscreen mode

-m will conveniently create a migration file as well. Adonis makes use of migrations to create and modify the database schema. There is no need to manually create the table in your database. This provides several benefits like version control, or making use of these migration files in our tests!

Running the test again reveals the next step, which is related to the database.

Knex: run
$ npm install sqlite3 --save
Error: Cannot find module 'sqlite3'
Enter fullscreen mode Exit fullscreen mode

If you haven't taken a look into .env.testing, this is the environment used for testing. By default it uses sqlite. Even though you plan on using a different database for actual development (like mysql), using sqlite is a good choice for testing as it keeps your tests fast.

This step might come to a surprise for some. No, we are not mocking out the database layer, instead we have a test database that we can migrate and reset on the fly. And with sqlite, it's all extremely light weight. The less we have to mock the more our tests are actually testing. And Adonis makes it an absolute breeze.

So let's install sqlite like the error message suggested.

npm install sqlite3 --save
Enter fullscreen mode Exit fullscreen mode

Running the test again shows us Error: SQLITE_ERROR: no such table: threads. Yes, we haven't created the table yet, but we do have a migration file for threads. What we have to do is tell vow to run all our migrations at the start of the tests, and roll everything back at the end.

We do this in vowfile.js. Everything is already there in fact, we just have to uncomment some lines.

14 -> const ace = require('@adonisjs/ace')
37 -> await ace.call('migration:run', {}, { silent: true })
60 -> await ace.call('migration:reset', {}, { silent: true })
Enter fullscreen mode Exit fullscreen mode

Running the test again reveals the next error ModelNotFoundException: E_MISSING_DATABASE_ROW: Cannot find database row for Thread model.

Makes sense, because right now, the controller is not inserting the thread into the database.

So let's head over to the controller and that part.

'use strict'

const Thread = use('App/Models/Thread')

class ThreadController {
    async store({ request, response }) {
        const thread = await Thread.create(request.only(['title', 'body']))
        return response.json({ thread })
    }
}

module.exports = ThreadController
Enter fullscreen mode Exit fullscreen mode

Running the test will now return another error related to the insertion.

'Error: insert into `threads` (`body`, `created_at`, `title`, `updated_at`) values (\'body\', \'2019-09-01 12:51:02\', \'test title\', \'2019-09-01 12:51:02\') - SQLITE_ERROR: table threads has no column named body',
Enter fullscreen mode Exit fullscreen mode

The table currently does not contain any column called body.

The solution is to add the new column to the up method in the migrations file that ends with _thread_schema.js.

    this.create('threads', (table) => {
      table.increments()
      table.text('body')
      table.timestamps()
    })
Enter fullscreen mode Exit fullscreen mode

Running the test will return a very similar error regarding the column title. So let's also add it to the migrations file.

    this.create('threads', (table) => {
      table.increments()
      table.string('title')
      table.text('body')
      table.timestamps()
    })
Enter fullscreen mode Exit fullscreen mode

And before you know it, the tests are green!

Now if you try to hit the actual endpoint during development it will complain that the table "threads" does not exist, that's because you have to run the migrations for your dev/prod environment yourself using adonis migration:run.

Refactoring

TDD consists of three stages, red - green - refactor. So let's refactor both the app as well as the tests and make sure that everything is still green. This is the beauty of TDD, it gives you confidence in your refactorings, thus making it safe, easy and fun.

Let's first get rid of the console.log in our test. We no longer need it.
Next, I am pretty confident I want to keep the controller resourceful, means it only has the default CRUD actions. So let's head over to routes.js and change out

Route.post('threads', 'ThreadController.store')
Enter fullscreen mode Exit fullscreen mode

with

Route.resource('threads', 'ThreadController').only(['store'])
Enter fullscreen mode Exit fullscreen mode

Not really necessary at this point, but what I want to show is that you can now run the tests again and have a confirmation that your refactorings didn't cause any side-effects. That's confidence!


Summary

We have our first test running! Next time we take a look at how we can resolve an issue with tests accidentally using the inserted data from other tests and model factories!

Oldest comments (2)

Collapse
 
iamfeek profile image
Muhammad Syafiq Hanafee

Hey man! Awesome guide!

Small hiccup, the adonis run migrations command did not work for me.

adonis migration:run worked for me.

Cheers!

Collapse
 
michi profile image
Michael Z

Thanks! I fixed it.