Course contentsShow
← Node.js
Lesson 1572 of 1,98014. Testing Methodologies and Quality AssuranceFree lesson

Concurrency and Race Condition Testing

Identify logic flaws by intentionally triggering concurrent updates to shared resources.

In this lesson, you will learn how to identify logic flaws in your Node.js applications by intentionally triggering concurrent updates to shared resources to expose race conditions.

The Problem: Non-Atomic Operations

In Node.js, even though your JavaScript runs in a single-threaded event loop, race conditions frequently occur when asynchronous operations access a shared resource (like a database or an in-memory object) in multiple steps. If a process reads a value, performs an await (yielding control back to the event loop), and then writes a new value based on that stale read, your application enters an inconsistent state.

Triggering the Race Condition

To test for this, you must bypass your standard serial test flow and intentionally fire off multiple requests simultaneously. The goal is to force the Node.js event loop to context-switch between your operations.

Consider an endpoint that decrements an inventory count:

// A vulnerable implementation
async function decrementStock(productId) {
  const item = await db.products.find(productId); // Step 1: Read
  if (item.stock > 0) {
    await db.products.update(productId, { stock: item.stock - 1 }); // Step 2: Write
  }
}

If you call this function twice in rapid succession, both instances might read item.stock = 1 before either has finished the update. To catch this, your test suite should use Promise.all() to saturate the event loop:

it('should not allow over-selling due to race conditions', async () => {
  const productId = 'test-item-123';
  // Attempt 5 simultaneous deductions
  const tasks = [1, 2, 3, 4, 5].map(() => decrementStock(productId));
  await Promise.all(tasks);

  const finalItem = await db.products.find(productId);
  // If the logic is flawed, finalItem.stock will be incorrect
  expect(finalItem.stock).toBeGreaterThanOrEqual(0);
});

Why This Works

By using Promise.all(), you ensure all requests are initiated within the same tick of the event loop. This maximizes the probability that the asynchronous await points will overlap across requests. If your code is not atomic—meaning the read-modify-write cycle is not protected by database-level transactions or advisory locks—the test will reliably fail, proving your concurrency logic is flawed.

Key Takeaway: Concurrency testing requires forcing overlapping execution flows to ensure your application remains consistent even when multiple asynchronous processes attempt to mutate the same data simultaneously.