How to test NestJS Guards

There are plenty of articles out there discussing different testing strategies for NestJS, and even the official documentation has a section dedicated to it, so why should you care about yet another one? If you have spent some time reading or searching for that topic, you may have noticed a shortage of deeper discussions. Many articles only scratch the surface of unit tests; even the official docs don't go further than general guidelines. You'll also probably see too much bad advice, like mocking services and database connections everywhere (want to know why it's bad advice? Check out my previous article on How to test External Dependencies with Nest). To remedy this lack of testing resources, I started writing a Series of Testing NestJS Articles. I'll present different strategies (and their cons and pros) for each type of construct in NestJS: Services, Guards, Interceptors, Pipes, Middlewares, etc. We'll start with Guards, as they are widely used, but their tests are often overlooked. TL:DR If you're just looking for a quick summary and the source code you check out this repository. The general idea is: By default, create integration tests for your Guards simulating a fake endpoint, which should be defined in a test controller within a test-only module; Extract business logic or algorithms to the domain layer and test it with unit tests; Finally, create higher-level e2e tests for the happy paths and one "negative" scenario for the endpoints that should be guarded. Example Feature and Background Consider an application named "WordWiz", created to help users generate high-quality marketing content, blog posts, and social media captions using AI. However, the level of access depends on the user's subscription tier: Free Tier: Limited access, basic AI models, and a low word count limit. Basic Plan: Access to better AI models, higher word limits, and additional templates. Premium Plan: Unlimited content generation, advanced AI models, and premium integrations (e.g., SEO analysis, API access). That application exposes three endpoints so far, each with a different subscription plan requirement: POST content/generate: Generates AI-powered content (Free+) GET content/templates: Retrieves available content templates (Basic+) GET content/analytics: Provides engagement insights for generated content We'll use the SubscriptionGuard to determine whether a user can access content. The flow diagram below illustrates how it should be implemented: For simplicity's sake, we won't use a JWT to store the user's subscription information (which would be ideal), but a straightforward x-user-subscription header. Finally, we have all we need to start writing the tests. Tests CAP Theorem Before discussing implementation details, I'd like to quickly describe the characteristics of each type of test and when we should use them. According to Vladimir Khorikov, there are four pillars of good automated tests: Protection Against Regressions - How much do your tests protect you against bugs? Resistance to Refactoring - How well can your tests resist internal refactors without breaking? Fast Feedback - How fast does your test execute? Maintainability - How easy is maintaining the test code? Each type of test - unit, integration, and e2e - scores higher or lower in each of the pillars above. However, we can't have all of them at the same time - very much like the CAP theorem, but for tests: The Resistance to Refactoring should always be maxed out, as we don't want brittle tests. However, we must choose between tests that run fast enough (unit) or tests that provide higher protection (e2e). We'll cover each type in the following sections with examples and further explanations. Integration Test Starting with integration tests is good because: We can see the test's entire data and decision flow. We have a good trade-off between protection and feedback speed. The SubscriptionGuard observable behavior must be tested for all intended cases (not just the endpoints I mentioned in the last section). Hence, I start by creating a testing module, a test controller, and a test application so we can emulate the following workflow: A request is sent to a test endpoint that requires a subscription plan The SubscriptionGuard accepts or denies the request The client receives an error or success response Let's see how that works - defining a subscriptions.guard.spec.ts file: // file: src/authz/subscriptions.guard.spec.ts import { Controller, Get, INestApplication, Module, UseGuards, } from '@nestjs/common'; import { SubscriptionsGuard } from './subscriptions.guard'; import { Test } from '@nestjs/testing'; import * as request from 'supertest'; import { App } from 'supertest/types'; import { RequiresSubscription } from './subscription.decorator'; import { Subscription } from './subscriptions'; import { SUBSCRIPTION_HEADER } from './subscription.header'; @Co

Apr 23, 2025 - 15:05
 0
How to test NestJS Guards

There are plenty of articles out there discussing different testing strategies for NestJS, and even the official documentation has a section dedicated to it, so why should you care about yet another one?

If you have spent some time reading or searching for that topic, you may have noticed a shortage of deeper discussions. Many articles only scratch the surface of unit tests; even the official docs don't go further than general guidelines. You'll also probably see too much bad advice, like mocking services and database connections everywhere (want to know why it's bad advice? Check out my previous article on How to test External Dependencies with Nest).

To remedy this lack of testing resources, I started writing a Series of Testing NestJS Articles. I'll present different strategies (and their cons and pros) for each type of construct in NestJS: Services, Guards, Interceptors, Pipes, Middlewares, etc. We'll start with Guards, as they are widely used, but their tests are often overlooked.

TL:DR

If you're just looking for a quick summary and the source code you check out this repository. The general idea is:

  1. By default, create integration tests for your Guards simulating a fake endpoint, which should be defined in a test controller within a test-only module;
  2. Extract business logic or algorithms to the domain layer and test it with unit tests;
  3. Finally, create higher-level e2e tests for the happy paths and one "negative" scenario for the endpoints that should be guarded.

Example Feature and Background

Consider an application named "WordWiz", created to help users generate high-quality marketing content, blog posts, and social media captions using AI. However, the level of access depends on the user's subscription tier:

  • Free Tier: Limited access, basic AI models, and a low word count limit.
  • Basic Plan: Access to better AI models, higher word limits, and additional templates.
  • Premium Plan: Unlimited content generation, advanced AI models, and premium integrations (e.g., SEO analysis, API access).

That application exposes three endpoints so far, each with a different subscription plan requirement:

  • POST content/generate: Generates AI-powered content (Free+)
  • GET content/templates: Retrieves available content templates (Basic+)
  • GET content/analytics: Provides engagement insights for generated content

We'll use the SubscriptionGuard to determine whether a user can access content. The flow diagram below illustrates how it should be implemented:

SubscriptionGuard Flow Diagram

For simplicity's sake, we won't use a JWT to store the user's subscription information (which would be ideal), but a straightforward x-user-subscription header. Finally, we have all we need to start writing the tests.

Tests CAP Theorem

Before discussing implementation details, I'd like to quickly describe the characteristics of each type of test and when we should use them.

According to Vladimir Khorikov, there are four pillars of good automated tests:

  • Protection Against Regressions - How much do your tests protect you against bugs?
  • Resistance to Refactoring - How well can your tests resist internal refactors without breaking?
  • Fast Feedback - How fast does your test execute?
  • Maintainability - How easy is maintaining the test code?

Each type of test - unit, integration, and e2e - scores higher or lower in each of the pillars above. However, we can't have all of them at the same time - very much like the CAP theorem, but for tests:

Depiction of unit, integration, and end-to-end tests and their relation with the pillars of good automated tests

The Resistance to Refactoring should always be maxed out, as we don't want brittle tests. However, we must choose between tests that run fast enough (unit) or tests that provide higher protection (e2e). We'll cover each type in the following sections with examples and further explanations.

Integration Test

Starting with integration tests is good because:

  1. We can see the test's entire data and decision flow.
  2. We have a good trade-off between protection and feedback speed.

The SubscriptionGuard observable behavior must be tested for all intended cases (not just the endpoints I mentioned in the last section). Hence, I start by creating a testing module, a test controller, and a test application so we can emulate the following workflow:

  1. A request is sent to a test endpoint that requires a subscription plan
  2. The SubscriptionGuard accepts or denies the request
  3. The client receives an error or success response

Let's see how that works - defining a subscriptions.guard.spec.ts file:

// file: src/authz/subscriptions.guard.spec.ts
import {
  Controller,
  Get,
  INestApplication,
  Module,
  UseGuards,
} from '@nestjs/common';
import { SubscriptionsGuard } from './subscriptions.guard';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { App } from 'supertest/types';
import { RequiresSubscription } from './subscription.decorator';
import { Subscription } from './subscriptions';
import { SUBSCRIPTION_HEADER } from './subscription.header';

@Controller()
@UseGuards(SubscriptionsGuard)
class TestController {
  @Get('/free')
  public freeEndpoint() {
    return 'success';
  }

  @RequiresSubscription(Subscription.Basic)
  @Get('/basic')
  public basicEndpoint() {
    return 'success';
  }

  @RequiresSubscription(Subscription.Premium)
  @Get('/premium')
  public premiumEndpoint() {
    return 'success';
  }
}

@Module({
  controllers: [TestController],
  providers: [SubscriptionsGuard],
})
class TestModuleForGuard {}

describe('SubscriptionsGuard', () => {
  let testApp: INestApplication<App>;

  beforeAll(async () => {
    const testingModule = await Test.createTestingModule({
      imports: [TestModuleForGuard],
    }).compile();

    testApp = testingModule.createNestApplication();
    await testApp.init();
  });

  afterAll(async () => {
    await testApp.close();
  });

  test('A Free user can access Free endpoints', async () => {
    await request(testApp.getHttpServer())
      .get('/free')
      .expect(200)
      .expect('success');
  });

  test('A Basic user can access Free and Basic endpoints', async () => {
    await request(testApp.getHttpServer())
      .get('/free')
      .set(SUBSCRIPTION_HEADER, Subscription.Basic)
      .expect(200)
      .expect('success');

    await request(testApp.getHttpServer())
      .get('/basic')
      .set(SUBSCRIPTION_HEADER, Subscription.Basic)
      .expect(200)
      .expect('success');
  });


  test('A Basic user cannot access Premium endpoints', async () => {
    await request(testApp.getHttpServer())
      .get('/premium')
      .set(SUBSCRIPTION_HEADER, Subscription.Basic)
      .expect(403);
  });
})

A lot is going on in this test case, so let's break it down:

  1. TestController - This is the controller that showcases how the Guard is supposed to be registered, how we can define an endpoint's subscription prerequisite (the @RequiresSubscription() decorator), and three endpoints exemplifying the different requirements.

  2. TestModuleForGuard - The testing module that defines how we should define the SubscriptionsGuard as a provider, and the one used to start a testingModule.

  3. beforeAll hook - We initialize the testing module and start a local HTTP server with the testing endpoints registered.

  4. Free, Basic, and Premium plan test cases — These three cover the basic assumptions of how our SubscriptionsGuard should work. Within each test case, you can see we either set the SUBSCRIPTION_HEADER (which is just an alias for x-user-subscription) or don't send a header at all. Depending on the endpoint requirements, we have a success or failure response.

These tests don't cover all possible scenarios, but they are enough for now to illustrate an initial implementation for our Guard:

// file: src/authz/subscriptions.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { Subscription, SubscriptionEnum } from './subscriptions';
import { SUBSCRIPTION_KEY } from './subscription.decorator';
import { SUBSCRIPTION_HEADER } from './subscription.header';

@Injectable()
export class SubscriptionsGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const requiredSubscription =
      this.reflector.getAllAndOverride<SubscriptionEnum>(SUBSCRIPTION_KEY, [
        context.getHandler(),
        context.getClass(),
      ]);

    if (!requiredSubscription) {
      return true; // Equivalent to the Free plan - no need for subscriptions
    }

    const userSubscription = context.switchToHttp().getRequest().headers[
      SUBSCRIPTION_HEADER
    ];

    switch (requiredSubscription) {
      case Subscription.Basic: {
        return (
          userSubscription === Subscription.Basic ||
          userSubscription === Subscription.Premium
        );
      }
      case Subscription.Premium: {
        return userSubscription === Subscription.Premium;
      }
      default: {
        return false;
      }
    }
  }
}

With that implemented, we already have all of our tests passing:

PASS src/authz/subscriptions.guard.spec.ts

From this point, you can also include the remaining test cases, which can be consulted here.

The steps above should suffice for a basic guard implementation that doesn't require much testing / don't have a complex enough logic. But can we go further? Let's find out in the next section.

Unit Testing the Subscriptions Logic - Or the "Humble Object" Pattern

Although the integration test cases above cover most cases in which we could be interested, it's worth exploring how we can further extract the domain logic from the Guard and test it in isolation, granting faster feedback. This is especially useful when, for some reason, our Guards are harder to test, and that's when the Humble Object Pattern comes into play.

In our scenario, we can apply that pattern by extracting the subscription comparison logic to its own file and testing it as follows:

// file: src/authz/subscription.spec.ts
import { subscription } from './subscriptions';

describe('Subscription', () => {
  test('A Basic subscription allows Basic features', () => {
    const result = subscription('BASIC').allows('BASIC');
    expect(result).toBe(true);
  });
  test('A Basic subscription does not allow Premium features', () => {
    const result = subscription('BASIC').allows('PREMIUM');
    expect(result).toBe(false);
  });
  test('A Premium subscription allows Basic features', () => {
    const result = subscription('PREMIUM').allows('BASIC');
    expect(result).toBe(true);
  });
  test('A Premium subscription allows Premium features', () => {
    const result = subscription('PREMIUM').allows('PREMIUM');
    expect(result).toBe(true); 
  });
});

And the corresponding implementation:

// file: src/authz/subscriptions.ts
export const Subscription = {
  Basic: 'BASIC',
  Premium: 'PREMIUM',
} as const;

export type SubscriptionEnum = (typeof Subscription)[keyof typeof Subscription];

export const subscription = (sub: SubscriptionEnum) => ({
  allows: (feature: SubscriptionEnum) => {
    if (sub === Subscription.Premium) {
      return true;
    }
    if (sub === Subscription.Basic && feature === Subscription.Basic) {
      return true;
    }
    return false;
  },
});

export function isSubscription(sub: string): sub is SubscriptionEnum {
  return Object.values(Subscription).includes(sub as SubscriptionEnum);
}

ℹ️Note: The types of subscriptions are encoded in an object instead of an enum because Enums might be considered harmful

Now, the SubscriptionsGuard implementation can be simplified by importing our new subscription function:

// file: src/authz/subscriptions.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { SUBSCRIPTION_KEY } from './subscription.decorator';
import { SUBSCRIPTION_HEADER } from './subscription.header';
import {
  isSubscription,
  subscription,
  SubscriptionEnum,
} from './subscriptions';

@Injectable()
export class SubscriptionsGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const requiredSubscription = this.reflector.getAllAndOverride<
      SubscriptionEnum | undefined
    >(SUBSCRIPTION_KEY, [context.getHandler(), context.getClass()]);

    if (!requiredSubscription) {
      return true;
    }

    const userSubscription: string = context.switchToHttp().getRequest()
      .headers[SUBSCRIPTION_HEADER];

    if (!isSubscription(userSubscription)) {
      return false;
    }

    return subscription(userSubscription).allows(requiredSubscription);
  }
}

At last, we can execute all tests to ensure our behavior stays the same:

 $ npm run test
 PASS  src/authz/subscription.spec.ts
 PASS  src/authz/subscriptions.guard.spec.ts

In the next (and last) section, we'll analyze how to test our guard using actual "production" endpoints as we specified in the "Requirements" section.

When the Rubber hits the road with E2E Tests

This entire structure was designed to fulfill our "WordWiz" example app subscription requirements. So, let's recall what endpoints we have to implement:

  • POST content/generate: Generates AI-powered content (Free+)
  • GET content/templates: Retrieves available content templates (Basic+)
  • GET content/analytics: Provides engagement insights for generated content

We can describe the expected behavior examples as follows:

  1. POST content/generate:
    1. A Free user has access
    2. A Basic plan user has access
    3. A Premium plan user has access
  2. GET content/templates:
    1. A Free user does not have access
    2. A Basic plan user has access
    3. A Premium plan user has access
  3. GET content/analytics:
    1. A Free user does not have access
    2. A Basic plan user does not have access
    3. A Premium plan user has access

We already have unit tests that guarantee higher subscription requirements (like Premium+) deny access to the lower subscription plans. So, we can focus on the highest subscription requirement for each endpoint:

// file: test/app.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
import { SUBSCRIPTION_HEADER } from '../src/authz/subscription.header';
import { Subscription } from '../src/authz/subscriptions';

describe('AppController (e2e)', () => {
  let app: INestApplication;

  beforeEach(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  it('/content/generate (POST) - generates content successfully', () => {
    return request(app.getHttpServer())
      .post('/content/generate')
      .set(SUBSCRIPTION_HEADER, Subscription.Basic)
      .send()
      .expect(201)
  });

  it('/content/templates (GET) - returns a list of templates', () => {
    return request(app.getHttpServer())
      .get('/content/templates')
      .set(SUBSCRIPTION_HEADER, Subscription.Basic)
      .send()
      .expect(200)
  });

  it('/content/templates (GET) - denies request when user does not have basic plan', () => {
    return request(app.getHttpServer())
      .get('/content/templates')
      .send()
      .expect(403);
  });

  it('/content/analytics (GET) - provides engagement insights for generated content', () => {
    return request(app.getHttpServer())
      .get('/content/analytics')
      .set(SUBSCRIPTION_HEADER, Subscription.Premium)
      .send()
      .expect(200)
  });

  it('/content/analytics (GET) - denies request when user does not have premium plan', () => {
    return request(app.getHttpServer())
      .get('/content/analytics')
      .set(SUBSCRIPTION_HEADER, Subscription.Basic)
      .send()
      .expect(403);
  });
});

Notice we are not asserting the response body as it's not really important for our use case (but you can check out how it could be tested here). Finally, we can specify the actual controller implementation:

// file: src/content/content.controller.ts
import { Controller, Get, Post, UseGuards } from '@nestjs/common';
import { RequiresSubscription } from '../authz/subscription.decorator';
import { SubscriptionsGuard } from '../authz/subscriptions.guard';

@Controller('content')
@UseGuards(SubscriptionsGuard)
export class ContentController {
  @Post('generate')
  generateContent() {
    return {
      content: 'Generated content goes here',
    };
  }

  @RequiresSubscription('BASIC')
  @Get('templates')
  getTemplates() {
    return [
      {
        id: '1',
        name: 'Template 1',
        description: 'Description of Template 1',
      },
    ];
  }

  @RequiresSubscription('PREMIUM')
  @Get('analytics')
  getAnalytics() {
    return {
      generatedArticles: 25,
      averageEngagementRate: 0.74,
      topPerformingArticles: [
        {
          title: 'How to Boost Your SEO with AI',
          views: 1200,
          shares: 340,
          likes: 250,
        },
      ],
      suggestedImprovements: [
        'Use more questions in headlines to increase engagement.',
      ],
    };
  }
}

Conclusion

In this article, we went beyond the basics of testing in NestJS by diving deep into how to effectively test Guards. We discussed:

  1. Why integration tests are a great starting point.
  2. How to simulate real-world scenarios using a test controller.
  3. How to apply the Humble Object pattern to extract domain logic.
  4. How to implement e2e tests for endpoints using guards.
  5. How to balance protection, speed, and maintainability using the Tests CAP Theorem.

The result is a more robust, maintainable, and clear testing strategy for Guards — one that avoids over-mocking and keeps your codebase reliable.

But this is just the beginning. This article is part of a broader series on testing NestJS applications, where I’ll explore how to apply similar principles to services, interceptors, pipes, middlewares, and beyond. Stay tuned for more practical guides and real-world examples that will help you master testing in NestJS. See you in the next one