Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mutant still survive although test failed #4582

Open
AkiraNoob opened this issue Nov 11, 2023 · 9 comments
Open

Mutant still survive although test failed #4582

AkiraNoob opened this issue Nov 11, 2023 · 9 comments
Labels
⁉ Question Further information is requested

Comments

@AkiraNoob
Copy link

Question

Hello everyone, this is my first question so if i make any mistake, pls let me know. Peace
Back to the question, can s.o explain for me why this mutant keep survived although when i try that mutant to the code itself, the test is failed.
My repo: https://github.com/AkiraNoob/kiem_chung

Test case:

    describe('Given valid payload', () => {
      const spyedRefreshCreate = jest.spyOn(RefreshTokenModel, 'create');
      const spyedRefreshDelete = jest.spyOn(RefreshTokenModel, 'deleteMany');
      const spyedFindUser = jest.spyOn(UserModel, 'findOne');
      const spyedBcryptCompare = jest.spyOn(bcryptCommon, 'bcryptCompareSync');
      it('should return statusCode 200 and data should contain {token, refreshToken} and message is "Login successfully"', async () => {
        const data = {
          token: {
            token: expect.any(String),
            expires: expect.any(String),
          },
          refreshToken: {
            token: expect.any(String),
            expires: expect.any(String),
          },
        };

        await expect(authService.loginWithEmailAndPassword(mockLocalLoginPayload)).resolves.toStrictEqual({
          statusCode: EHttpStatus.OK,
          data,
          message: expect.stringMatching('Login successfully'),
        });

        expect(spyedBcryptCompare).toHaveBeenCalledWith(mockLocalLoginPayload.password, expect.any(String));
        expect(spyedBcryptCompare).toHaveReturnedWith(true);

        expect(spyedFindUser).toHaveBeenCalledWith({
          email: expect.any(String),
        });

        expect(spyedRefreshDelete).toHaveBeenCalledWith({
          userId: expect.any(mongoose.Types.ObjectId),
        });

        return expect(spyedRefreshCreate).toHaveBeenCalledWith({
          userId: expect.any(mongoose.Types.ObjectId),
          refreshToken: expect.any(String),
          expiredAt: expect.any(String),
        });
      });
    });

service:

loginWithEmailAndPassword: async (
    reqBody: TLocalLoginPayload,
  ): Promise<TServiceResponseType<{ token: TReturnJWTType; refreshToken: TReturnJWTType }>> => {
    const user = await UserModel.findOne({ email: reqBody.email }).select('+password');

    if (!user) {
      throw new AppError(EHttpStatus.BAD_REQUEST, 'Wrong email');
    }

    if (!bcryptCompareSync(reqBody.password, user.password)) {
      throw new AppError(EHttpStatus.BAD_REQUEST, 'Wrong password');
    }

    const userData = {
      id: user._id.toString(),
      email: user.email,
      fullName: user.fullName,
    };

    const token = signJWT(userData);
    const refreshToken = signRefreshJWT(userData);

    await RefreshTokenModel.deleteMany({ userId: new mongoose.Types.ObjectId(userData.id) });

    await RefreshTokenModel.create({
      userId: user._id,
      refreshToken: refreshToken.token,
      expiredAt: refreshToken.expires,
    });

    return {
      data: {
        token,
        refreshToken,
      },
      statusCode: EHttpStatus.OK,
      message: 'Login successfully',
    };
  },

mutant

- await RefreshTokenModel.deleteMany({ userId: new mongoose.Types.ObjectId(userData.id) });
+ await RefreshTokenModel.deleteMany({})

Stryker environment

+-- @stryker-mutator/core@7.3.0
+-- @stryker-mutator/jest-runner@7.3.0
+-- @stryker-mutator/typescript-checker@7.3.0
+--jest@29.7.0
+--supertest@6.3.3

Additional context

my stryker config:

{
  "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json",
  "_comment": "This config was generated using 'stryker init'. Please take a look at: https://stryker-mutator.io/docs/stryker-js/configuration/ for more information.",
  "packageManager": "npm",
  "reporters": ["html", "progress"],
  "testRunner": "jest",
  "testRunner_comment": "Take a look at (missing 'homepage' URL in package.json) for information about the jest plugin.",
  "coverageAnalysis": "perTest",
  "mutate": ["src/**"],
  "ignoreStatic": true,
  "checkers": ["typescript"],
  "tsconfigFile": "tsconfig.json",
  "typescriptChecker": {
    "prioritizePerformanceOverAccuracy": false
  }
}

my jest config

/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  testMatch: ['**/**/*.test.ts'],
  forceExit: true,
  verbose: true,
  clearMocks: true,
  setupFiles: ['<rootDir>/.jest/setEnv.ts'],
  coverageDirectory: 'reports/coverage',
  testTimeout: 5000,
};

Also node that i currently use node 18.18.2

@AkiraNoob AkiraNoob added the ⁉ Question Further information is requested label Nov 11, 2023
@odinvanderlinden
Copy link
Contributor

Hi @AkiraNoob , thanks for opening this issue. I'll have a look at your source code and try to figure out what is happening.

@odinvanderlinden
Copy link
Contributor

So far there is one thing that has caught my attention, when i run Stryker with the change that should let a test fail. The initial test run fails. This means that the problem is not with finding the test. Besides that i don't know what's going wrong here. @nicojs do you have a clue?

@nicojs
Copy link
Member

nicojs commented Dec 8, 2023

I think i've found the issue. Your tests seem to share state.

This change:

--- a/server/src/api/service/auth.service.ts
+++ b/server/src/api/service/auth.service.ts
@@ -68,7 +68,7 @@ const authService = {
       fullName: verifiedRefreshToken.fullName,
     };
 
-    await RefreshTokenModel.deleteMany({ userId: new mongoose.Types.ObjectId(userData.id) });
+    await RefreshTokenModel.deleteMany({});

Makes your tests fail when you run them all serially (the test 'Given valid payload should return statusCode 200 and data should contain {token, refreshToken} and message is "Login successfully"' fails), but the failing test passes when you focus it:

--- a/server/__test__/api/service/auth.service.test.ts
+++ b/server/__test__/api/service/auth.service.test.ts
@@ -55,7 +55,7 @@ describe('Testing auth service', () => {
       const spyedRefreshDelete = jest.spyOn(RefreshTokenModel, 'deleteMany');
       const spyedFindUser = jest.spyOn(UserModel, 'findOne');
       const spyedBcryptCompare = jest.spyOn(bcryptCommon, 'bcryptCompareSync');
-      it('should return statusCode 200 and data should contain {token, refreshToken} and message is "Login successfully"', async () => {
+      it.only('should return statusCode 200 and data should contain {token, refreshToken} and message is "Login successfully"', async () => {

Focussing tests is what StrykerJS does when you set coverageAnalysis to "perTest".

I would suggest making your unit test independent of each other. You can do this by removing the global state, and instead initialing variables inside beforeEach hooks.

For example:

--- a/server/__test__/api/service/auth.service.test.ts
+++ b/server/__test__/api/service/auth.service.test.ts
@@ -11,20 +11,26 @@ import UserModel from '../../../src/model/user';
 import { TLocalLoginPayload, TRegisterPayload } from '../../../src/types/api/auth.types';
 import { TUserSchema } from '../../../src/types/schema/user.schema.types';
 
-const userPayload: TUserSchema = {
-  email: 'tester.001@company.com',
-  password: bcryptCommon.bcryptHashSync('Tester@001'),
-  fullName: 'Tester 001',
-  avatar: 's3_img_string',
-  dateOfBirth: new Date(),
-};
-
-const mockRegisterPayload: TRegisterPayload = {
-  ...omit(userPayload, ['avatar', 'dateOfBirth']),
-  password: 'Tester@001',
-};
-
-const mockLocalLoginPayload: TLocalLoginPayload = omit(mockRegisterPayload, 'fullName');
+let userPayload: TUserSchema;
+let mockRegisterPayload: TRegisterPayload;
+let mockLocalLoginPayload: TLocalLoginPayload;
+
+beforeEach(() => {
+  userPayload = {
+    email: 'tester.001@company.com',
+    password: bcryptCommon.bcryptHashSync('Tester@001'),
+    fullName: 'Tester 001',
+    avatar: 's3_img_string',
+    dateOfBirth: new Date(),
+  };
+
+  mockRegisterPayload = {
+    ...omit(userPayload, ['avatar', 'dateOfBirth']),
+    password: 'Tester@001',
+  };
+
+  mockLocalLoginPayload = omit(mockRegisterPayload, 'fullName');
+});

@AkiraNoob
Copy link
Author

thanks @nicojs and @odinvanderlinden for help me out, i really appriciate it! Seem like that is the reason

@AkiraNoob
Copy link
Author

AkiraNoob commented Dec 19, 2023

sorry for re-open this issue but somehow i think it not solves my case, i provide a brief version of my code:
image

const generateMockPayload = () => {
  const userPayload: TUserSchema = {
    email: 'tester.001@company.com',
    password: bcryptCommon.bcryptHashSync('Tester@001'),
    fullName: 'Tester 001',
    avatar: 's3_img_string',
    dateOfBirth: new Date(),
  };

  const mockRegisterPayload: TRegisterPayload = {
    ...omit(userPayload, ['avatar', 'dateOfBirth']),
    password: 'Tester@001',
  };

  const mockLocalLoginPayload: TLocalLoginPayload = omit(mockRegisterPayload, 'fullName');

  return {
    userPayload,
    mockRegisterPayload,
    mockLocalLoginPayload,
  };
};

describe('Testing auth service', () => {
  beforeAll(async () => {
    const mongoServer = await MongoMemoryServer.create();
    await mongoose.connect(mongoServer.getUri());
  });

  afterAll(async () => {
    await mongoose.disconnect();
    await mongoose.connection.close();
  });

  beforeEach(() => {
    jest.clearAllMocks();
  });

  describe('Register service', () => {
    afterAll(async () => {
      const { userPayload } = generateMockPayload();

      await UserModel.deleteMany({ email: userPayload.email });
    });

    describe('Given valid payload', () => {
      it('should return statusCode 200 and data is null and message is "Register successfully"', async () => {
        const { mockRegisterPayload } = generateMockPayload();

        const spyedUserModelCreate = jest.spyOn(UserModel, 'create');
        const resolveData = {
          statusCode: EHttpStatus.OK,
          data: null,
          message: expect.stringMatching('Register successfully'),
        };

        await expect(authService.register(mockRegisterPayload)).resolves.toStrictEqual(resolveData);
        return expect(spyedUserModelCreate).toHaveBeenCalledWith({
          ...mockRegisterPayload,
          password: expect.any(String),
        });
      });
    });

  });
});

@AkiraNoob AkiraNoob reopened this Dec 19, 2023
@AkiraNoob
Copy link
Author

i belive that if it runs to return statement, it means that API is resolves successfully and MUST return message in image

@AkiraNoob
Copy link
Author

AkiraNoob commented Dec 20, 2023

Sorry for my mistake, i try research more and try some approaches: #2989 #3068 and trouble shooting but my issue still occurs. I have created a smaller repository to focus on a single unit here. You can see that if i run

npm run mutation-test

1 mutant still survive but the test is failed (i try to isolate the testcase as suggest). Hope this repository will help to solve my issue

@AkiraNoob
Copy link
Author

Sorry for my hurry but any update on this?

@odinvanderlinden
Copy link
Contributor

Hi @AkiraNoob sorry for the late response, I haven't had a lot of time to work on Stryker lately. I have taken a look at your new repository. And it seems that you still make use of shared state. For every single test you should clear your in memory database. This is not the case right now. For instance the after all method where you clear the user should be a AfterEacht method to make sure you start every test with a clean sheet. I hope this helps!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
⁉ Question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants