The Hidden Danger of Shared State in Angular Unit Tests

Recently, while working on a legacy Angular project, I encountered an interesting — and admittedly a little frustrating — problem. It reinforced an important lesson about writing truly independent unit tests, and I thought it would be valuable to share the experience.

We all know one of the core principles of unit testing:

Every test should be independent. The outcome of a test should never depend on the execution order of other tests.

But what happens when this rule is accidentally broken?


The Symptom: Passing Tests... Until You Change the Order

At first, everything seemed fine. All the Jasmine unit tests were passing — green across the board. However, based on the behavior I was observing, curiosity led me to change the order of the tests within the spec file.

To my surprise, some tests started failing. Even stranger, when I restored the original order, they would pass again!

This was a huge red flag. A reliable test suite should behave the same way, regardless of the order in which tests are executed.


The Root Cause: A Shared Mutable Object

After some investigation, I discovered the issue:

  • There was a common object defined outside of any beforeEach block.
  • Every test suite (describe) and individual test case (it) was sharing the same reference to this object.
  • Tests were mutating this shared object during execution.
  • Because the object wasn't re-initialized before each test, state carried over from one test to another.

Essentially, tests were leaking into each other, causing unpredictable behavior depending on the test execution order.

Here’s a simplified example of what I found:

// shared object defined outside
const sharedData = { value: 0 };

describe('Test Suite A', () => {
  it('should set value to 1', () => {
    sharedData.value = 1;
    expect(sharedData.value).toBe(1);
  });
});

describe('Test Suite B', () => {
  it('should have value 0 initially', () => {
    expect(sharedData.value).toBe(0); // This fails if Test Suite A ran before
  });
});
        

In this example, if Test Suite A runs first, it modifies sharedData.value to 1. Then, when Test Suite B runs, it incorrectly finds sharedData.value already changed, resulting in a test failure.

This explains why the results changed based on the order of execution.


The Solution: Reinitialize State with beforeEach

The fix was simple but crucial: move the object initialization inside a beforeEach block, ensuring each test gets a fresh copy of the object.

Here’s the corrected version:

let sharedData: { value: number };

beforeEach(() => {
  sharedData = { value: 0 }; // re-initialize before every test
});

describe('Test Suite A', () => {
  it('should set value to 1', () => {
    sharedData.value = 1;
    expect(sharedData.value).toBe(1);
  });
});

describe('Test Suite B', () => {
  it('should have value 0 initially', () => {
    expect(sharedData.value).toBe(0); // Now this test will always pass
  });
});
        

Now, no matter the order in which the tests are executed, they are completely isolated and independent.


Key Takeaways

  • Tests should never share mutable state unless it is intentionally controlled.
  • Always use beforeEach to reinitialize dependencies and ensure a clean state for each test.
  • A trustworthy test suite must behave consistently, no matter how tests are ordered.

To view or add a comment, sign in

More articles by Rezaul Karim

  • Why You Should Never Write Business Logic Inside a Component's Constructor Function (Angular)

    Many developers would say that making the creation of any object costly is a bad practice. When there are many tasks to…

  • Atomicity in Database

    Atom শব্দের অর্থ পরমাণু। এমন নামকরণের কারণ নিশ্চয় এই যে, একসময় মনে করা হতো এটাই পদার্থের ক্ষুদ্রতম কণা, যাকে আর ভাঙ্গা…

    3 Comments
  • Recent Implementation of Adapter Design Pattern

    Recently, I successfully implemented the Adapter Pattern to seamlessly integrate Google Map's Direction service into…

  • 5 Common Mistakes I Have Seen in Writing REST API

    Including the method name in the URI.Example : Here, using a method name like getUser is just unnecessary.

    2 Comments

Insights from the community

Explore topics