Skip to main content

.NET MSTest Framework

Introduction

Microsoft's MSTest is the built-in testing framework for .NET that allows developers to create and run automated tests for their applications. MSTest (also known as Visual Studio Test) provides a comprehensive set of tools and libraries for testing .NET code, making it an excellent choice for beginners in the .NET ecosystem.

As one of the oldest testing frameworks for .NET, MSTest is deeply integrated with Visual Studio and offers a straightforward approach to testing your applications. In this guide, we'll explore how to set up MSTest, write effective tests, and utilize its features to ensure your code works as expected.

Getting Started with MSTest

Setting Up MSTest in a Project

To get started with MSTest in your .NET project, you need to install the required NuGet packages:

bash
dotnet add package Microsoft.NET.Test.Sdk
dotnet add package MSTest.TestAdapter
dotnet add package MSTest.TestFramework

Alternatively, you can create a new MSTest project using the .NET CLI:

bash
dotnet new mstest -n MyTestProject

Project Structure

A typical MSTest project structure looks like:

MyTestProject/
├── MyTestProject.csproj
└── UnitTest1.cs

The generated UnitTest1.cs file will contain a basic test class:

csharp
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace MyTestProject
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
// Your test code here
}
}
}

Core MSTest Components

Test Class

In MSTest, a test class is marked with the [TestClass] attribute and contains one or more test methods. Each test class should focus on testing a specific component or functionality of your application.

csharp
[TestClass]
public class CalculatorTests
{
// Test methods go here
}

Test Methods

Test methods are marked with the [TestMethod] attribute and contain the code to verify that your application behaves as expected.

csharp
[TestMethod]
public void Add_TwoNumbers_ReturnsSum()
{
// Arrange
var calculator = new Calculator();

// Act
int result = calculator.Add(5, 3);

// Assert
Assert.AreEqual(8, result);
}

Setup and Teardown

MSTest provides attributes for setup and teardown operations:

  • [TestInitialize]: Runs before each test method
  • [TestCleanup]: Runs after each test method
  • [ClassInitialize]: Runs once before any tests in the class
  • [ClassCleanup]: Runs once after all tests in the class

Here's an example:

csharp
[TestClass]
public class DatabaseTests
{
private Database _db;

[TestInitialize]
public void Setup()
{
_db = new Database();
_db.Connect();
}

[TestCleanup]
public void Cleanup()
{
_db.Disconnect();
}

[ClassInitialize]
public static void ClassSetup(TestContext context)
{
// Setup shared resources
}

[ClassCleanup]
public static void ClassCleanup()
{
// Clean up shared resources
}

[TestMethod]
public void TestDatabaseOperation()
{
// Test code
}
}

Writing Effective MSTest Tests

Assertions

MSTest provides a rich set of assertion methods through the Assert class:

csharp
[TestMethod]
public void AssertionDemo()
{
// Basic equality
Assert.AreEqual(42, CalculateValue());

// Reference equality
Assert.AreSame(expectedObject, actualObject);

// Boolean assertions
Assert.IsTrue(condition);
Assert.IsFalse(condition);

// Null checks
Assert.IsNull(value);
Assert.IsNotNull(value);

// Type checks
Assert.IsInstanceOfType(value, typeof(string));

// Exception testing
Assert.ThrowsException<ArgumentNullException>(() => MethodThatThrows(null));

// String assertions
Assert.AreEqual("expected", actual, ignoreCase: true);
Assert.IsTrue(string.Contains("substring"));

// Collection assertions
CollectionAssert.AreEqual(expectedCollection, actualCollection);
CollectionAssert.Contains(collection, item);
}

Testing Exceptions

To test that a method properly throws an expected exception:

csharp
[TestMethod]
public void Divide_ByZero_ThrowsDivideByZeroException()
{
// Arrange
var calculator = new Calculator();

// Act & Assert
Assert.ThrowsException<DivideByZeroException>(() => calculator.Divide(10, 0));
}

Data-Driven Tests

MSTest supports data-driven tests through the [DataRow] attribute, allowing you to run the same test with different inputs:

csharp
[DataTestMethod]
[DataRow(1, 1, 2)]
[DataRow(5, 3, 8)]
[DataRow(0, 0, 0)]
[DataRow(-1, -1, -2)]
public void Add_WithVariousInputs_ReturnsSum(int a, int b, int expected)
{
// Arrange
var calculator = new Calculator();

// Act
int result = calculator.Add(a, b);

// Assert
Assert.AreEqual(expected, result);
}

Real-World MSTest Example

Let's look at a more comprehensive example testing a ShoppingCart class:

First, our implementation class:

csharp
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}

public class ShoppingCart
{
private List<Product> _items = new List<Product>();

public IReadOnlyList<Product> Items => _items.AsReadOnly();

public void AddItem(Product product)
{
if (product == null)
throw new ArgumentNullException(nameof(product));

_items.Add(product);
}

public void RemoveItem(int productId)
{
var product = _items.FirstOrDefault(p => p.Id == productId);
if (product != null)
_items.Remove(product);
}

public decimal CalculateTotal()
{
return _items.Sum(item => item.Price);
}

public void Clear()
{
_items.Clear();
}
}

Now, let's write tests for our ShoppingCart:

csharp
[TestClass]
public class ShoppingCartTests
{
private ShoppingCart _cart;
private Product _product1;
private Product _product2;

[TestInitialize]
public void Setup()
{
_cart = new ShoppingCart();

_product1 = new Product
{
Id = 1,
Name = "Test Product 1",
Price = 10.00m
};

_product2 = new Product
{
Id = 2,
Name = "Test Product 2",
Price = 15.50m
};
}

[TestMethod]
public void AddItem_ValidProduct_AddsToCart()
{
// Act
_cart.AddItem(_product1);

// Assert
Assert.AreEqual(1, _cart.Items.Count);
Assert.AreEqual(_product1, _cart.Items[0]);
}

[TestMethod]
public void AddItem_NullProduct_ThrowsArgumentNullException()
{
// Act & Assert
Assert.ThrowsException<ArgumentNullException>(() => _cart.AddItem(null));
}

[TestMethod]
public void RemoveItem_ExistingProduct_RemovesFromCart()
{
// Arrange
_cart.AddItem(_product1);
_cart.AddItem(_product2);

// Act
_cart.RemoveItem(_product1.Id);

// Assert
Assert.AreEqual(1, _cart.Items.Count);
Assert.AreEqual(_product2, _cart.Items[0]);
}

[TestMethod]
public void RemoveItem_NonexistentProduct_DoesNothing()
{
// Arrange
_cart.AddItem(_product1);

// Act
_cart.RemoveItem(999); // Non-existent product ID

// Assert
Assert.AreEqual(1, _cart.Items.Count);
}

[TestMethod]
public void CalculateTotal_MultipleProducts_ReturnsSumOfPrices()
{
// Arrange
_cart.AddItem(_product1); // $10.00
_cart.AddItem(_product2); // $15.50

// Act
decimal total = _cart.CalculateTotal();

// Assert
Assert.AreEqual(25.50m, total);
}

[TestMethod]
public void Clear_WithProducts_EmptiesCart()
{
// Arrange
_cart.AddItem(_product1);
_cart.AddItem(_product2);

// Act
_cart.Clear();

// Assert
Assert.AreEqual(0, _cart.Items.Count);
}
}

Advanced MSTest Features

Test Categories

You can categorize tests using the [TestCategory] attribute:

csharp
[TestMethod]
[TestCategory("Integration")]
public void DatabaseConnection_ValidCredentials_ConnectsSuccessfully()
{
// Test code
}

This allows you to run specific categories of tests:

bash
dotnet test --filter "TestCategory=Integration"

Ignoring Tests

Sometimes you may need to temporarily disable a test:

csharp
[TestMethod]
[Ignore("Fix this test after updating the API")]
public void TemporarilyBrokenTest()
{
// Test code
}

Timeout

You can set a timeout for tests that should complete within a specific time:

csharp
[TestMethod]
[Timeout(1000)] // 1000 milliseconds
public void TimeLimitedTest()
{
// Test code that should complete quickly
}

Expected Exceptions

Besides Assert.ThrowsException, you can also specify expected exceptions with the [ExpectedException] attribute:

csharp
[TestMethod]
[ExpectedException(typeof(DivideByZeroException))]
public void Divide_ByZero_ThrowsException()
{
var calculator = new Calculator();
calculator.Divide(10, 0); // Should throw DivideByZeroException
}

Testing Asynchronous Code

MSTest supports testing asynchronous methods:

csharp
[TestMethod]
public async Task FetchDataAsync_ValidRequest_ReturnsData()
{
// Arrange
var client = new DataClient();

// Act
var result = await client.FetchDataAsync("api/data");

// Assert
Assert.IsNotNull(result);
Assert.AreEqual("expected data", result.Value);
}

Running MSTest Tests

Running Tests from the Command Line

Use the .NET CLI to run your tests:

bash
# Run all tests in the project
dotnet test

# Run specific tests
dotnet test --filter "FullyQualifiedName~ShoppingCartTests"

# Run tests with a specific category
dotnet test --filter "TestCategory=UnitTest"

Running Tests in Visual Studio

In Visual Studio, you can run tests from the Test Explorer window, which offers features like:

  • Running all tests
  • Running specific tests
  • Debugging tests
  • Viewing test results and output

Best Practices for MSTest

  1. Follow the AAA Pattern: Arrange, Act, Assert

    csharp
    [TestMethod]
    public void Example_AAAPattern()
    {
    // Arrange
    var sut = new SystemUnderTest();

    // Act
    var result = sut.MethodToTest();

    // Assert
    Assert.AreEqual(expected, result);
    }
  2. Name tests clearly: Use a naming convention like MethodName_Scenario_ExpectedBehavior

  3. Keep tests independent: Tests shouldn't depend on each other's state or execution order

  4. One assertion concept per test: Focus each test on verifying one specific behavior

  5. Use test doubles (mocks/stubs): To isolate the system under test from external dependencies

  6. Test edge cases: Include boundary conditions and error scenarios

  7. Keep tests fast: Slow tests discourage frequent test runs

Summary

MSTest is a powerful and integrated testing framework for .NET applications that provides:

  • Easy setup and integration with Visual Studio
  • Comprehensive assertion capabilities
  • Support for test initialization and cleanup
  • Features for data-driven testing
  • Tools for categorizing and filtering tests
  • Support for asynchronous code testing

By following the patterns and practices outlined in this guide, you can create effective tests that help ensure your .NET applications work correctly and reliably.

Additional Resources

Practice Exercises

  1. Create a test project for a simple Calculator class with basic arithmetic operations
  2. Write tests for a string utility class that includes methods for string manipulation
  3. Create data-driven tests for a temperature conversion class
  4. Write tests for a method that parses and validates email addresses
  5. Test a service class that depends on external dependencies (using mocks)

By practicing these exercises, you'll gain confidence and proficiency in using MSTest for your .NET applications!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)