Stacks and Queues: When to Use Them in Test Scenarios

You've just added Dictionaries and HashSets to your C# toolkit, giving you powerful ways to handle data lookups and ensure uniqueness. While List<T> and Dictionary<TKey, TValue> will be your daily workhorses, sometimes you encounter problems where the order of processing is critical.

For these special cases, C# provides two beautifully simple yet powerful collections: the Stack<T> and the Queue<T>. They don't have all the bells and whistles of a List<T>; instead, they are designed to do one thing exceptionally well: enforce a specific order for adding and removing items.

Let's explore these specialized tools and discover the unique testing scenarios where they truly shine.

Robots with cinema tickets in their hands standing at the cinema entrance in a line

The Stack – Last In, First Out (LIFO)

A Stack<T> operates on the Last In, First Out (LIFO) principle. The last item you add to the stack is always the first one to be removed.

The perfect analogy is a stack of plates. You place a new plate on top of the stack, and when you need a plate, you take the one from the top. You don't pull one from the bottom (unless you enjoy chaos and broken dishes!). Another great example is your browser's "back" button history – the last page you visited is the first one you go back to.

Primary Stack Operations

A stack has a very restricted and intentional set of methods:

  • Push(item): Adds an item to the top of the stack.
  • Pop(): Removes the item from the top of the stack and returns it. Trying to pop from an empty stack will cause an exception.
  • Peek(): Lets you look at the item at the top of the stack without removing it.

Let's see it in action:

using System;
using System.Collections.Generic;
 
class Program
{
    static void Main(string[] args)
    {
        var browserHistory = new Stack();
 
        // User navigates through pages
        browserHistory.Push("Home Page");
        browserHistory.Push("Product Category Page");
        browserHistory.Push("Product Details Page"); // This is now at the top
 
        Console.WriteLine($"Currently on: {browserHistory.Peek()}"); // Peeks at "Product Details Page"
 
        // User hits the back button
        string currentPage = browserHistory.Pop();
        Console.WriteLine($"Navigating back from: {currentPage}");
        Console.WriteLine($"Now on: {browserHistory.Peek()}");
 
        currentPage = browserHistory.Pop();
        Console.WriteLine($"Navigating back from: {currentPage}");
        Console.WriteLine($"Now on: {browserHistory.Peek()}");
        Console.ReadKey();
    }
}   

Expected Output:

Currently on: Product Details Page
Navigating back from: Product Details Page
Now on: Product Category Page
Navigating back from: Product Category Page
Now on: Home Page

The LIFO behavior is clear: the last URL pushed onto the stack is the first one to be popped off.

The Queue – First In, First Out (FIFO)

A Queue<T> is the opposite of a stack. It operates on the FFirst In, First Out (FIFO) principle. The first item added to the queue is the first one to be removed.

The best real-world analogy is a checkout line at a supermarket. The first person to get in line is the first person to be served. Other examples include a printer queue or a support ticket system – tasks are handled in the order they are received.

Primary Queue Operations

A queue's methods reflect its FIFO nature:

  • Enqueue(item): Adds an item to the end of the queue.
  • Dequeue(): Removes the item from the beginning of the queue and returns it. Trying to dequeue from an empty queue will cause an exception.
  • Peek(): Lets you look at the item at the beginning of the queue without removing it.

Let's model a print job queue:

using System;
using System.Collections.Generic;
 
class Program
{
    static void Main(string[] args)
    {
        var printJobs = new Queue();
 
        // Users send documents to the printer
        printJobs.Enqueue("Report.docx");      // Arrives first
        printJobs.Enqueue("Presentation.pptx"); // Arrives second
        printJobs.Enqueue("Spreadsheet.xlsx");   // Arrives third
 
        Console.WriteLine($"Next job to print: {printJobs.Peek()}");
 
        // Printer finishes a job
        string completedJob = printJobs.Dequeue();
        Console.WriteLine($"Finished printing: {completedJob}");
        Console.WriteLine($"Now printing: {printJobs.Peek()}");
 
        completedJob = printJobs.Dequeue();
        Console.WriteLine($"Finished printing: {completedJob}");
        Console.WriteLine($"Now printing: {printJobs.Peek()}");
        Console.ReadKey();
    }
}   

Expected Output:

Next job to print: Report.docx
Finished printing: Report.docx
Now printing: Presentation.pptx
Finished printing: Presentation.pptx
Now printing: Spreadsheet.xlsx

The FIFO behavior is exactly what you'd expect from a queue: items are processed in the order they were added.

When to Use Stacks in Test Automation

While less common than Lists or Dictionaries, Stacks have some very clever use cases in testing.

Verifying "Undo" or "Back" Functionality

This is the classic scenario. If you are testing an application with a multi-step wizard or a feature with an "undo" button, a stack is the perfect data structure to model the expected behavior.

// PSEUDOCODE for a test
var performedActions = new Stack<string>();
 
// Your test performs actions and pushes them onto the stack
app.EnterUsername("myUser");
performedActions.Push("Entered Username");
 
app.EnterPassword("myPass");
performedActions.Push("Entered Password");
 
app.ClickLoginButton();
performedActions.Push("Clicked Login");
 
// Now, test the "back" or "undo" functionality
app.ClickBackButton();
string lastAction = performedActions.Pop(); // Should be "Clicked Login"
// Assert that the app state has reverted to before the login click.
 
app.ClickBackButton();
lastAction = performedActions.Pop(); // Should be "Entered Password"
// Assert that the app state has reverted to before the password was entered.    

Managing State in Recursive Navigation

This is a more advanced scenario. If you are writing a test that needs to recursively navigate a tree-like structure (like a file system, an organization chart, or a complex menu system), a stack is often used internally to keep track of the path taken and to backtrack correctly. You might not use one directly in your test, but the algorithms for such traversals often do!

When to Use Queues in Test Automation

While a List<T> is flexible enough for most of your test automation tasks, a Queue<T> provides unique advantages in specific scenarios where the order of processing and the "consumption" of items are key parts of your test logic.

Resource Pool Management with Actual Consumption

Imagine your tests need to use a limited pool of shared resources, like temporary user accounts, API tokens, or even browser driver instances. A queue is perfect for managing this pool. Each test "consumes" a resource by dequeuing it, making it unavailable to other concurrent tests. When the test is finished, it can return the resource to the pool by enqueuing it again.

// In your test setup, you might populate a queue of available test users.
var availableTestUsers = new Queue<TestUser>();
availableTestUsers.Enqueue(new TestUser("user1"));
availableTestUsers.Enqueue(new TestUser("user2"));
 
// Your test "checks out" a user from the pool.
var userForThisTest = availableTestUsers.Dequeue();
 
// ... execute test logic with userForThisTest ...
 
// After the test, the user resource can be returned to the queue.
availableTestUsers.Enqueue(userForThisTest);    

Using a queue here clearly communicates the intent of "taking" a resource from the pool and makes your test logic for managing shared resources safer.

Testing Queue-Based Systems

The most direct use case is when the system you are testing is a queue itself. Many applications use queuing systems for tasks like processing orders, sending emails, or handling background jobs. To test this, your code can mirror the system's behavior perfectly by using a Queue<T>.

// 1. You enqueue expected events in your test's queue.
var expectedEvents = new Queue<string>();
expectedEvents.Enqueue("ORDER_PLACED");
expectedEvents.Enqueue("PAYMENT_PROCESSED");
expectedEvents.Enqueue("ORDER_SHIPPED");
 
// 2. Your test interacts with the application, triggering these events.
// ... code that places an order ...
 
// 3. You verify that the application generated the events in the correct FIFO order.
var firstEvent = GetNextEventFromApplication(); // Fictional method
Assert.That(expectedEvents.Dequeue(), Is.EqualTo(firstEvent.Name)); // Should be "ORDER_PLACED"
 
var secondEvent = GetNextEventFromApplication();
Assert.That(expectedEvents.Dequeue(), Is.EqualTo(secondEvent.Name)); // Should be "PAYMENT_PROCESSED"    

The Right Tool for the Job

Could you achieve these things with a List<T>? Yes, you could use list.Add() and then list.RemoveAt(0) to simulate a queue. However, this is less performant for large lists and, more importantly, it doesn't clearly communicate your intent. When another developer sees you using a Queue, they immediately understand the strict FIFO processing and consumption behavior you require. Using the right, specialized data structure makes your code cleaner, more readable, and self-documenting.

Key Takeaways

  • Stack<T> is a collection that operates on the Last In, First Out (LIFO) principle. Use it when you need to process items in the reverse order they were added.
  • The main operations for a Stack are Push(item) to add to the top, and Pop() to remove from the top.
  • Queue<T> is a collection that operates on the First In, First Out (FIFO) principle. Use it when you need to process items in the exact order they were added.
  • The main operations for a Queue are Enqueue(item) to add to the end, and Dequeue() to remove from the beginning.
  • While List<T> is more versatile, using Stacks and Queues for LIFO/FIFO scenarios makes your code's intent clearer and more efficient for those specific operations.

Sharpen Your Tools

What's Next?

Excellent! You now have a great understanding of the most common C# collections and their specific strengths. Now that you can store and organize your data effectively, it's time to learn how to manipulate it with power and elegance. To unlock C#'s most powerful data-querying feature, LINQ, we first need to understand the clever feature that makes it possible: Extension Methods.