Skip to content

Testing Style Guide

Contents:

Angular Testing: Jasmine and NgMocks

Leveraging Multiple Describe Blocks for Modular Unit Tests

Our Angular applications are unit tested using Jasmine and ng-mocks, structuring tests efficiently is important for maintainability, readability, and the overall quality of our test suites. One effective practice is the use of multiple describe blocks within a single unit test file.

Why Multiple Describe Blocks?

  1. Improved Structure and Readability: Multiple describe blocks allow you to group related tests together logically. You can have separate describe blocks for different components, services, or even specific functionalities within a component. For example, in a component's test suite, if a flow of logic is different based on whether a component's instance variable indicating if a user is logged in or not can have separate describe blocks for each scenario.

  2. Isolation of Tests: Isolating tests in separate describe blocks ensures that the setup, execution, and teardown of one group of tests do not affect others. This isolation is key to maintaining test integrity and avoiding interdependencies that can lead to flaky tests.

Below is a simple example of how testing with multiple describe blocks can be used to improve the structure and readability of your tests.

Single vs Multiple Describe Blocks Example

In this example, we have a component called MyComponent. This component has two functions: proxiedUserFunctionality() and nonProxiedUserFunctionality(). The component calls these functions based on whether a user is Proxying or not. It also has a button that is disabled when a user is Proxying.

Example with one describe block:

describe("MyComponent", () => {
  let component: MyComponent;

  beforeEach(() => {
  // Common Test Setup
  // Mock the User service and other dependencies if needed.
  // Mock the User service to return a user that is Proxying
  // Mock the component to return a value that disables the Favorite button
  component = MockRender(MyComponent).point.componentInstance;
  });

  it("should create", () => {
    expect(component).toBeTruthy();
  });

  it("should call proxiedUserFunctionality() when User is Proxying", () => {
    // ... unit test code ...

    // this unit test relies on the setup being mocked to return a user that is Proxying

    expect(component.proxiedUserFunctionality).toHaveBeenCalled();
  });

  it("should not call proxiedUserFunctionality() when User is not Proxying", () => {
    // ... unit test code ...

    // this unit test relies on the setup being mocked to return a user that is NOT Proxying

    // The main issue with this is that in the setup,
    // a user that is Proxying is being mocked.
    // So to make this test pass, we have to modify the return value of the
    // User service to return a user that is not Proxying so this test will pass
    // but the other unit tests that rely on the User service being mocked to return
    // a user that is Proxying will not fail

    // code to modify the return value of the User service to return a user that is not Proxying

    expect(component.nonProxiedUserFunctionality).toHaveBeenCalled();
  });

  it("should disable Favorite button when User is Proxying", () => {
    // ... unit test code ...

    // In here we have to modify the return value of the component to return a value that disables the Favorite button

    expect(component.favoriteButton.disabled).toBe(true);
  });
});

An improved example with multiple describe blocks:

describe("MyComponent", () => {

  let component: MyComponent;

  describe("User is Proxying", () => {
  beforeEach(() => {
  // Common Test Setup
  // Mock the User service and other dependencies if needed.
  // Mock the User service to return a user that is Proxying
  component = MockRender(MyComponent).point.componentInstance;
  });

      it("should create", () => {
        expect(component).toBeTruthy();
      });

      it("should call proxiedUserFunctionality() when User is Proxying", () => {
        // ... unit test code ...
        expect(component.proxiedUserFunctionality).toHaveBeenCalled();
        expect(component.nonProxiedUserFunctionality).not.toHaveBeenCalled();
      });

      it("should disable Favorite button when User is Proxying", () => {
        // ... unit test code ...
        expect(component.favoriteButton.disabled).toBe(true);
      });

      // Other unit tests that rely on the User service being mocked to return a user that is Proxying
      // Here we can test in isolation the functionality of the component when the User is Proxying

  });

  describe("User is NOT Proxying", () => {
  beforeEach(() => {
  // Common Test Setup
  // Mock the User service and other dependencies if needed.
  // Mock the User service to return a user that is NOT Proxying
  component = MockRender(MyComponent).point.componentInstance;
  });

      it("should create", () => {
        expect(component).toBeTruthy();
      });

      it("should not call proxiedUserFunctionality() when User is not Proxying", () => {
        // ... unit test code ...
        // this unit test relies on the User service being mocked to return a user that is not Proxying
        expect(component.nonProxiedUserFunctionality).toHaveBeenCalled();
        expect(component.proxiedUserFunctionality).not.toHaveBeenCalled();
      });

      it("should enable Favorite button when User is not Proxying", () => {
        // ... unit test code ...
        expect(component.favoriteButton.disabled).toBe(false);
      });

      // Additional tests for the scenario where the User is NOT Proxying

  });
  });

Best Practices for Using Multiple Describe Blocks

  • Consistent Naming Conventions: Use clear and descriptive names for each describe block to immediately convey the focus of the tests contained within.
  • Shared Setup and Teardown: Utilize beforeEach and afterEach within each describe block for setup and teardown specific to that block’s tests, promoting test independence.
  • Avoid Deep Nesting: While multiple describe blocks are beneficial, avoid overly deep nesting. It can make tests harder to follow and maintain.

Dotnet Testing: XUnit and Moq

TODO