V4 - Unit Testing

System Information
  • Strapi Version: 4
  • Operating System: macos
  • Database: postgres
  • Node Version: 14
  • NPM Version:
  • Yarn Version:

Hi, i was trying to setup unit testing for my strapi app built with strapi v4. Having a lot of issues regarding to initialize the strapi instance, give the mock user permissions to hit the desired endpoint always recieving 403 so i forced to give the mock user the permissions manually in the test environment but this is not an acceptable approch at all. And after that the test keepa failing.

1 Like

Can you share how you got testing to work at all. Iā€™ve had no success in using the v3 instructions to set up a test suite for v4.

Thanks,
Steve

Facing the same problem. I was able to initialize the strapi importing from @strapi

const Strapi  = require('@strapi/strapi');
# instead of  strapi like in the docs
const Strapi = require('strapi');
# this is the only thing i have been able to do so far
# when i try to populate routes, middlewares and create server
await instance.app
  .use(instance.router.routes()) // populate KOA routes
   .use(instance.router.allowedMethods()); // populate KOA methods
    instance.server = http.createServer(instance.app.callback());
# I get  error

I hope the community can provide an update documentation for solving this.

@Mohammed_Akram Could you share how youā€™ve initialized Strapi for testing? I think it will help the others in the thread.

As for permissions, I would recommend you try one of my plugins config-sync.

With it you can export/import config data (like permissions). This way you can version control this data and also use it in your tests by importing before you run your tests.

Yes sure will do that

Hello, i will share my solution for the problem but this is might be no the optimal solution but this is how i tackled
1- The strapi instance initialization.
2- Granting the permissions for the Authenticated role to my controllers.
3- How to create a user and assign the role ā€˜Authenticatedā€™ to this mock user.

1- The strapi instance initialization is pretty much like @Aashis said but with minor modification. Should be like that

const Strapi = require("@strapi/strapi");
let instance;

async function setupStrapi() {
  if (!instance) {
    await Strapi().start();
    instance = strapi;
  }
  return instance;
}

to make an API call - this is different than the docs

  it("Should login user and return jwt token", async () => {
    const { user } = await getAuthenticatedUser(); // this is will be covered in point 3. 
    await request(strapi.server.httpServer) // app server is an instance of Class: http.Server
      .post("/api/auth/local")
      .set("accept", "application/json")
      .set("Content-Type", "application/json")
      .send({
        identifier: mockUser.email,
        password: mockUser.password,
      })
      .expect("Content-Type", /json/)
      .expect(200)
      .then(data => {
        expect(data.body.jwt).toBeDefined();
      });
  });

2- Granting the permissions for the Authenticated role to my controllers.

Iā€™m pretty sure that should be another optimal solution instead of the one that iā€™m currently using. But this is will work just fine but itā€™s an extra step after all.

i created a method called grantPrivilege to add the permissions to the Authenticated role. Or to allow the Authenticated user to use any controller if the permissions is granted.

here is the function

/**
 * Grants database `permissions` table that role can access an endpoint/controllers
 *
 * @param {int} roleID, 1 Autentihected, 2 Public, etc
 * @param {Object} body
 */
const grantPrivilege = async (roleID = 1) => {
  return strapi.plugin("users-permissions").service("role").updateRole(roleID, authenticatedRolePermissions); // i will add a sample for that below
};

the authenticatedRolePermissions is an object with the whole controllers or plugins that i have

const authenticatedRolePermissions = {
name: "Authenticated",
description: "Default role given to authenticated user.",
permissions: {
    "api::article": {
          controllers: {
               article: {
                   find: { enabled: true, policy: "" },
                   findOne: { enabled: true, policy: "" },
                   create: { enabled: true, policy: "" },
                   update: { enabled: true, policy: "" },
                   delete: { enabled: true, policy: "" },
// If there is any custom methods will need to add it here
                  myCustomMethod: { enable : true, policy: "" }
              }
          }
      }
  },
}

Then to complete the setup you will need to fire grantPrivilage in beforeAll jest method

beforeAll(async () => {
  await setupStrapi(); // singleton so it can be called many times
  await grantPrivilege();
});

This will allow the Authenticated role - this role is automatically assigned to any authenticated use -.

3- How to create a user and assign the role ā€˜Authenticatedā€™ to this mock user.

Last but not least to create a user with Authenticated role. To do that you can just use the code right below.

const createUserWithAuthenticatedRole = async (newUser = {}) => {
  const mockUserData = {
    username: "tester",
    email: "tester@test.com",
    provider: "local",
    password: "1234abc",
    confirmed: true,
    blocked: null,
  };

  const advanced = await strapi.store({ type: "plugin", name: "users-permissions", key: "advanced" }).get();

  const defaultRole = await strapi
    .query("plugin::users-permissions.role")
    .findOne({ where: { type: advanced.default_role } });

  mockUserData.role = defaultRole.id; // Assign the Authenticated role to the user

  const user = await strapi.plugins["users-permissions"].services.user.add({
    ...mockUserData,
    ...newUser,
  });
  return user;
};

/**
 * Create user with role Authenticated
 * @returns {Object}
 */

const getAuthenticatedUser = async () => {
  let user;

  user = await strapi.db.query("plugin::users-permissions.user").findOne({
    where: {
      email: "tester@test.com",
    },
    populate: { role: true },
  });

  if (!user) {
    user = await createUserWithAuthenticatedRole();
  }

  // Issue a token for this user - to test the private routes
  const jwt = strapi.plugin("users-permissions").service("jwt").issue({
    id: user.id,
  });

  return {
    user,
    jwt,
  };
};

module.exports = {
  getAuthenticatedUser,
  createUserWithAuthenticatedRole,
};
2 Likes

I have made some progress on this with a bit of digging. Similar but different to @m.akram I am using @strapi().load() to initialise the instance and then loading the routes. Iā€™m not sure how calling Strapi().start() would work since it would actually start the http server listening to the default port.

The result of strapi().load() is an app instance and inside there is the Koa server and the router. I found that I need to initialise the routes for the server before I could get a response. Having done that I can get successful response from /_health (204) and /admin (200) but calling my defined API at /api/jobs/ doesnā€™t work. Iā€™m wondering if this is because of the database not being properly initialised.

Hereā€™s my code:

const fs = require('fs');
const request = require('supertest');
const strapi = require('@strapi/strapi')

let server 

/** this code is called once before any test */
beforeAll(async () => {
  instance = await strapi().load(); 
  const app = instance.server.app
  app.use(instance.server.router.routes())
     .use(instance.server.router.allowedMethods())
  server = app.callback()
});

/** this code is called after all the tests are finished */
afterAll(async () => {
  const dbSettings = instance.config.get('database.connections.default.settings');
  
  //close server to release the db-file
  await instance.destroy();
  //delete test database after all tests
  if (dbSettings && dbSettings.filename) {
    const tmpDbFile = `${__dirname}/../${dbSettings.filename}`;
    if (fs.existsSync(tmpDbFile)) {
      fs.unlinkSync(tmpDbFile);
    }
  }
});

//Run test to make sure Strapi is defined.
it('strapi is defined', () => {
  expect(server).toBeDefined();
});


it('returns 204 from /_health', (done) => {
   request(server)
    .get('/_health')
    .expect(204)
    .end(done)
});

 it('returns a response from the admin dashboard', (done) => {
   request(server).get('/admin/')
   .expect(200)
   .end(done)
 })

 
 it('returns a response from the api', (done) => {
  request(server).get('/api/jobs/')
  .expect(200)
  .end(done)
})

Steve

1 Like

@stevecassidy - I tried your approach before the one that iā€™m using now - talking about load() instead of start() and yes itā€™s odd behaviour that i have to stop the development server to run the tests canā€™t do both together but this is the only way that i found to solve all the problems until the team update the documentation. Also, faced the same problem that my routes - the one that iā€™m using in the app - is not populating always receive 404.

Thanks for that @Mohammed_Akram, I went to look at what load() does that start() doesnā€™t do and it seems to be a call to server.mount() inside the .listen method. So I added in a call to mount and my api tests now work:

let instance 
let server 

beforeAll(async () => {
  instance = await strapi().load(); 
  instance.server.mount()
  server = instance.server.app.callback()
});

 it('returns a response from the api', (done) => {
  request(server).get('/api/jobs/')
  .expect(200)
  .end(done)
})

Steve

Iā€™ve begun some updates to the documentation to reflect what Iā€™ve found to work:

Iā€™ve not got the authentication test to work yet but the remainder are ok. One issue is that I consistently get open handles left behind - none of the suggested fixes work. Any ideas?

Steve

Thanks a lot!

Iā€™ve been struggling for a week with my first test to create and retrieve an object from the temp database.

Regards!

Hi,
can any one share the temp db configuration in test environment

Here it is :

File : config/env/test/database.js

const path = require("path");

module.exports = ({ env }) => ({
  connection: {
    client: "sqlite",
    connection: {
      filename: path.join(
        __dirname,
        "../../../",
        env("DATABASE_FILENAME", ".tmp/test.db")
      ),
    },
    useNullAsDefault: true,
  },
});

Thanks @NateR42

Hello,

do you have somewhere a working example with the latest strapi version? I tried to get this to work, but I have no chance yetā€¦

I started a clean project and followed the documentation from you @stevecassidy and used your database config @NateR42 , but still no chance.

I published my project to GitHub. If you have any ideas, Iā€™d be happy to hear from you. Sad that Strapi v4 is so ā€œoldā€ now, but there seems to be no real solution to the testing-problem.

Here is the repository with a configured CI, so that we can see, that this is not just not working on my machine :smiley: Add ci Ā· n2o/strapi-tests-failing@5bca782 Ā· GitHub

Hope we get this fixed soon :+1:

Hello, I am working on a new version of the unit test guide. As of now there is an example of unit tests on a function and route tests on a public route. I am working on documentation for testing an authenticated route. The two options would be using the Users and Permissions plugin authentication or authentication for an administrator. I am curious if there are any opinions on one of these options over the other?

I am excited to hear your feedback!

For me, personally, I prefer working with the users-permissions-Plugin, because these users are the ones accessing the API. Admin users typically access the admin panel, where I do not need to write tests (thatā€™s strapiā€™s job :wink: ).

1 Like

Hi,
where can I find a working example of the unit-test and routing-test? If one follows the guide from the strapi website and copies all the referenced snippets 1:1, it still simply failsā€¦

I am still struggling in getting a strapi instance, which can then be used for integration tests. Thatā€™s really annoyingā€¦ I am looking into the source code and am trying to see how the internal functions can be used, but it simply wonā€™t work. Currently, I get a strapi instance, but apparently it does not know anything about my APIs I defined in the very same projectā€¦

@n2o here is a link to a draft PR that revises the unit testing guide. I canā€™t promise that everything works but it should be OK for the Strapi instance and the most basic tests.

Hi, thanks for the link!

Jest tells me in this case (as well as in the unit-testing guide from the official website), that there are no tests found. I copied your snippets into an empty project created with npx create-strapi-app@latest my-project --quickstart

image

Link to better quality

Thanks for your help. As I understand the strapi and jest docs, the test files should be foundā€¦