Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Michael Z
Michael Z

Posted on • Originally published atmichaelzanggl.com

     

Adonis.js V6 - It's even better!

It's been 3.5 years since I created the seriesTDD course with Adonis.js using Adonis.js version 4.
Since then Adonis.js has gone through major releases, primarly the switch to TypeScript. I haven't used either Adonis.js v5 or v6, so I thought it would be interesting to try to recreate parts of the TDD course in the latest version by cross checking my old series with the up-to-date documentation.

Like back then, we will create an application that lets you manage threads, similar to a site like reddit.

During the making I realized how similar the versions are to each other, which is a good thing btw, so I will start by showing the final version and then the different parts it's made of.

Setup

You start off by initiating the project using the following command:

npm init adonisjs@latest adonis-tdd-v6

You can choose between web, api, and slim. we choose the api kit for this example which is meant for REST APIs. Web would be for fullstack applications.

Comparison to V4:

  • The setup has been streamlined by no longer requiring the globaladonis cli.
  • Testing no longer requires any additional installing and configurations

Writing our first test

Also explained here:https://docs.adonisjs.com/guides/http-tests

We start off by creating a test. While we can create files manually, Adonis.js also comes with easy to use commands to speed up development.

Executenode ace make:test thread and then choose functional, as we want to test an API route.

In this test, we make an API request to "/threads" and then assert that the response status is 200:

// tests/functional/thread.spec.tsimport{test}from"@japa/runner";test.group("Thread",()=>{test("can create threads",async({client})=>{constresponse=awaitclient.post("/threads").json({title:"test title",body:"body",});response.assertStatus(200);});});
Enter fullscreen modeExit fullscreen mode

We then proceed to create the actual controller usingnode ace make:controller threads and add the store method to the class:

// app/controllers/threads_controller.tsexportdefaultclassThreadsController{asyncstore(){}}
Enter fullscreen modeExit fullscreen mode

and we register this controller in the routes file:

// start/routes.tsimportrouterfrom"@adonisjs/core/services/router";importThreadsControllerfrom"#controllers/threads_controller";router.post("threads",[ThreadsController,"store"]);
Enter fullscreen modeExit fullscreen mode

Running the tests vianpm test,npm t, ornode ace test should already show a passing test.

Difference to V4:

  • Tests no longer require the "trait" function.
  • Controllers are now explictly imported in the routes file rather than a stringifed "ThreadController.store" which is great as it allows for faster navigation and prevents typo errors.
  • Files now use snake case instead of Pascal case

Creating the model

Now our test isn't doing anything yet so let's add the creation of threads.

We create the model usingnode ace make:model Thread -m. The flag-m also creates the database migration file to create the table.

We update the test to add the thread assertion:

// tests/functional/thread.spec.tsimport{test}from"@japa/runner";test.group("Thread",()=>{test("can create threads",async({client})=>{constresponse=awaitclient.post("/threads").json({title:"test title",body:"body",});response.assertStatus(200);// 👇 Changesconstthread=awaitThread.firstOrFail();response.assertBodyContains(thread.serialize({fields:["id","title","body"]}));});});
Enter fullscreen modeExit fullscreen mode

Run the test and it will tell you that the table doesn't exist. Adonis V6 uses sqlite by default and it's already set up for us. You can check outconfig/Database.ts for the configurations.

DIfference to V4:

  • no manual installation and set up of sqlite.

Now to make the table exist in this sqlite db, we have to run the database migrations at the start of tests.

This can be done intests/bootstrap.ts as explained here:https://docs.adonisjs.com/guides/database-tests

// tests/bootstrap.tsimporttestUtilsfrom"@adonisjs/core/services/test_utils";exportconstrunnerHooks:Required<Pick<Config,"setup"|"teardown">>={setup:[()=>testUtils.db().migrate()],teardown:[],};
Enter fullscreen modeExit fullscreen mode

Running the tests now will no longer complain that the table doesn't exist, but it will fail because "Thread.firstOrFail()" didn't return any data. So let's make our controller action create the thread!

// app/controllers/threads_controller.tsimporttype{HttpContext}from"@adonisjs/core/http";importThreadfrom"#models/thread";exportdefaultclassThreadsController{asyncstore({request,response}:HttpContext){constthread=awaitThread.create(request.only(["title","body"]));returnresponse.json(thread);}}
Enter fullscreen modeExit fullscreen mode

This part is identical with V4. Don't worry about the missing validation and authorization for this example, suffice to say, Adonis.js also has first class support for it.

The tests now fail withCannot define \"title\" on \"Thread\" model, since it is not defined as a model property so let's set up our model fields!

In the migration file, add the title and body fields like this:

// database/migrations/<timestamp>_create_threads_table.tsimport{BaseSchema}from"@adonisjs/lucid/schema";exportdefaultclassextendsBaseSchema{protectedtableName="threads";asyncup(){this.schema.createTable(this.tableName,(table)=>{table.increments("id");// 👇 Changestable.text("body").notNullable();table.string("title").notNullable();table.timestamp("created_at");table.timestamp("updated_at");});}asyncdown(){this.schema.dropTable(this.tableName);}}
Enter fullscreen modeExit fullscreen mode

We also declare the fields in the model:

// app/models/thread.tsimport{DateTime}from"luxon";import{BaseModel,column}from"@adonisjs/lucid/orm";exportdefaultclassThreadextendsBaseModel{@column({isPrimary:true})declareid:number;// 👇 Changes@column()declarebody:string;// 👇 Changes@column()declaretitle:"string;"@column.dateTime({autoCreate:true})declarecreatedAt:DateTime;@column.dateTime({autoCreate:true,autoUpdate:true})declareupdatedAt:DateTime;}
Enter fullscreen modeExit fullscreen mode

With this change, the tests should be green again!

Changes to V4:

  • To achieve type safety, we now need to specify the columns on the model as well.

Delete threads

Let's add another test to delete threads in our functional test file:

// tests/functional/thread.spec.tstest("can delete threads",async({client})=>{constthread=awaitThread.create({title:"\"test title\","body:"test body",});constresponse=awaitclient.delete(`threads/${thread.id}`);response.assertStatus(200);});
Enter fullscreen modeExit fullscreen mode

Back inroutes.ts we can replace writing out each route by using "router.resource" which will follow the convential CRUD routes and names (see details indocs):

// start/routes.tsrouter.resource("threads",ThreadsController).only(["store","destroy"]);
Enter fullscreen modeExit fullscreen mode

Next, we add the actual controller action as a method in the class:

// app/controllers/threads_controller.tsasyncdestroy({params}:HttpContext){constthread=awaitThread.findOrFail(params.id)awaitthread.delete()}
Enter fullscreen modeExit fullscreen mode

That works, but let's also test there are zero threads left in the DB.

// tests/functional/thread.spec.tsconstthreads=awaitThread.all();assert.equal(threads.length,0);
Enter fullscreen modeExit fullscreen mode

changes to v4:

  • it seems v6 is missing the Model.getCount() method to easily do a count(*). Hope it will be added back.

Running this will fail as there is still one thread apparently.
Let's try running this test in isolation:
node ace test --tests 'can delete threads'

Hey it works! This makes sense because the thread from the other test is still there.
We need to set up that the DB is reset after each test as well.

To do this, we need to add this to our test file:

// tests/functional/thread.spec.tsimport{test}from"@japa/runner";importThreadfrom"#models/thread";importtestUtilsfrom"@adonisjs/core/services/test_utils";test.group("Thread",(group)=>{group.each.setup(()=>testUtils.db().withGlobalTransaction());// ...});
Enter fullscreen modeExit fullscreen mode

This is very similar to V4, and again I wished if we could just set up something like this once in bootstrap.ts. Hey, I even prepared a PR for it back thenhttps://github.com/adonisjs/vow/pull/58.

But it's nothing we can't create ourselves.

For example, you could create a snippet for this, your own npm/ace command, or you could simply abstract this if you wanted in a new utility file:

// tests/db.tsimporttestUtilsfrom"@adonisjs/core/services/test_utils";import{Group}from"@japa/runner/core";import{test}from"@japa/runner";export{test}from"@japa/runner";exportconstgroup=(groupTitle:string,callback:(group:Group)=>void)=>{returntest.group(groupTitle,(group)=>{group.each.setup(()=>testUtils.db().truncate());returncallback(group);});};
Enter fullscreen modeExit fullscreen mode

Now we can just write tests like this:

// tests/functional/thread.spec.tsimport{test,group}from"#tests/db";group("Thread",()=>{test("create threads",async({client})=>{});});
Enter fullscreen modeExit fullscreen mode

But generally, I like not deviating too much from the framework.

Factories

In our tests we will need to create many fake instances of threads, like in the thread deletion test. Let's refactor this part a little using factories so we won't have to update this test again in the future when we add new fields.

First we create the factory for our thread table using:node ace make:factory thread and add the title and body columns:

// database\factories\thread_factory.tsimportfactoryfrom"@adonisjs/lucid/factories";importThreadfrom"#models/thread";exportconstThreadFactory=factory.define(Thread,async({faker})=>{return{title:faker.lorem.sentence(),body:faker.lorem.text(),};}).build();
Enter fullscreen modeExit fullscreen mode

With this, in the test, we can now refactor

// tests/functional/thread.spec.tsconstthread=awaitThread.create({title:"test title",body:"test body",});
Enter fullscreen modeExit fullscreen mode

to just:

// tests/functional/thread.spec.tsimport{ThreadFactory}from"#database/factories/thread_factory";constthread=awaitThreadFactory.create();
Enter fullscreen modeExit fullscreen mode

Note that because everything is TypeScript now, you can just typeThreadF... and it should auto suggest ThreadFactory so you don't have to write out the import statements for factories.

For the creation, where we pass title and body to the api client, we can also utilize the factory and themake method, which will only new up an instance but not persist it in the database (unlikecreate):

// tests/functional/thread.spec.tsconstinput=awaitfactories.threads.make();constresponse=awaitclient.post("/threads").json(input.serialize({fields:["title","body"]}));
Enter fullscreen modeExit fullscreen mode

auth middleware

So far any user can create threads, let's add some authorization to the routes.

First we create the new test:

// tests/functional/thread.spec.tstest("unauthenticated user cannot create threads",async({client})=>{constinput=awaitfactories.threads.make();constresponse=awaitclient.post("/threads").json(input.serialize({fields:["title","body"]}));response.assertStatus(401);});
Enter fullscreen modeExit fullscreen mode

Next we add the auth middleware to our routes:

// start/routes.tsimport{middleware}from"#start/kernel";importrouterfrom"@adonisjs/core/services/router";importThreadsControllerfrom"#controllers/threads_controller";router.resource("threads",ThreadsController).only(["store","destroy"]).use("*",middleware.auth());
Enter fullscreen modeExit fullscreen mode

That's basically it, but there's two tiny more steps to set upsession based auth setup for testing:

We add the plugin to tests/bootstrap.ts:

// tests/bootstrap.tsimport{authApiClient}from"@adonisjs/auth/plugins/api_client";// 👇 Changesimport{sessionApiClient}from"@adonisjs/session/plugins/api_client";exportconstplugins:Config["plugins"]=[assert(),apiClient(),pluginAdonisJS(app),authApiClient(app),// 👇 ChangessessionApiClient(app),];
Enter fullscreen modeExit fullscreen mode

We also create a new file in the root directory to store environment variables for the test environment:

// .env.testSESSION_DRIVER=memory;
Enter fullscreen modeExit fullscreen mode

The new test is passing now while the others fail.
To fix the other tests we need to send the API request with an authenticated user.


To do this, we first set up the user table.

A migration for users was created automatically, you can find in the database/migrations folder.

Let's create a factory for users:
node ace make:factory user

// database\factories\user_factory.tsimportfactoryfrom"@adonisjs/lucid/factories";importUserfrom"#models/user";exportconstUserFactory=factory.define(User,async({faker})=>{return{full_name:faker.internet.userName(),email:faker.internet.email(),password:faker.internet.password(),};}).build();
Enter fullscreen modeExit fullscreen mode

and we can now add the user to our two failing tests, for example:

// tests/functional/thread.spec.tsconstuser=awaitUserFactory.create();constresponse=awaitclient.delete(`threads/${thread.id}`).loginAs(user);
Enter fullscreen modeExit fullscreen mode

and it works! Sure it's not optimal that any user can delete any thread, but we are getting there...

Relationships

Next, we'll be linking the threads and user models together by adding the "user_id" to threads.
One user can have many threads. Instead of manually linking user_id (which we can totally do), we can encode the relationship in Adonis.js.

We first update the controller to create the thread through the user model:

// app/controllers/threads_controller.tsexportdefaultclassThreadsController{asyncstore({auth,request,response}:HttpContext){constthread=awaitauth.user!.related("threads").create(request.only(["title","body"]));returnresponse.json({thread});}// ...}
Enter fullscreen modeExit fullscreen mode

And we update both the user and the threads model to add the relationship and the new field:

// app/models/user.tsimport{BaseModel,column,hasMany}from"@adonisjs/lucid/orm";importtype{HasMany}from"@adonisjs/lucid/types/relations";importThreadfrom"./thread.js";exportdefaultclassUserextendscompose(BaseModel,AuthFinder){// ...other columns@hasMany(()=>Thread)declarethreads:HasMany<typeofThread>;}
Enter fullscreen modeExit fullscreen mode
// app/models/thread.tsexportdefaultclassThreadextendsBaseModel{// ...other columns@column()declareuserId:string;// Note we define the fields in camel case}
Enter fullscreen modeExit fullscreen mode

Next, we add the user_id and foreign key to the migration file:

// database/migrations/<timestamp>_create_threads_table.tstable.integer("user_id").unsigned().notNullable();table.foreign("user_id").references("id").inTable("users");
Enter fullscreen modeExit fullscreen mode

The only part that's missing now is to add user_id to the factory as well for tests.
What's interesting is we can also define our relationship there:

// database\factories\user_factory.tsexportconstUserFactory=factory.define(User,async({faker})=>{return{full_name:faker.internet.userName(),email:faker.internet.email(),password:faker.internet.password(),};})// 👇 Changes.relation("threads",()=>ThreadFactory).build();
Enter fullscreen modeExit fullscreen mode

And now we update our existing threads to use "UserFactory.with('threads')" instead of the "ThreadFactory" so it auto-establishes the relationship for us.

// tests/functional/thread.spec.tstest("can delete threads",async({assert,client})=>{constuser=awaitUserFactory.with("threads").create();constresponse=awaitclient.delete(`threads/${user.threads[0].id}`).loginAs(user);response.assertStatus(200);constthreads=awaitThread.all();assert.equal(threads.length,0);});
Enter fullscreen modeExit fullscreen mode

Closing

I'm really happy with Adonis.js V6! The documentation is very fleshed out, the development process is streamlined, the simple to use APIs from back then are all still here, it's fun to write code with it, and the type system is stellar.

I know this is missing some parts from the original TDD course like validation but I'm confident you can do it from here ;)

I do wish a plugin existed to sync models with factories and possibly migrations so there's not so many files to update when adding a field. I was able to hack together a "auto factory builder" quickly. Let me know if you are interested and I can share it!

Top comments(2)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
quyetcn05 profile image
Quyet Bui
  • Joined

I'm starting to get acquainted with adonisjs 6, can you share with me how to debug adonis project in webstorm, I tried but to no avail.

CollapseExpand
 
michi profile image
Michael Z
Software writer
  • Location
    Tokyo
  • Joined

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Software writer
  • Location
    Tokyo
  • Joined

More fromMichael Z

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp