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.

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.

1 Like

@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.

I am facing the same issues as @stevecassidy.
I am trying to setup unit testing with a strapi instance as specified in the docs here:

When I then run npm run test I get the following error:
Cannot find module 'strapi' from 'tests/helpers/strapi.js

When I change the import from require('strapi') to require('@strapi/strapi'), which is probably the new import syntax under v4, I still get an error though a different one:

Can someone help me setup unit testing, so I can start developing on a strapi v4 app?
I’m still relatively new to coding in general, so this is not something I can just simply come up with or derive myself.
Thank you!

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!