Lesson Sunday

This lesson is meant to be a handy reference for creating test-driven projects. We'll review what's needed to get MSTest up and running in a C# program, including naming conventions and requirements, project structure, best practices, and writing tests.

Project Structure


MSTest requires we set up our project in a specific way:

  • Create a parent directory named ProjectName.Solution.
  • Within it, add two subdirectories:
    • ProjectName
    • ProjectName.Tests
  • These directories are different "versions" of our project. The first is our production project and the second is our test project.
  • Each version of the project needs a .csproj configuration file.
    • Create a ProjectName.csproj file in the ProjectName subdirectory.
    • Create a ProjectName.Tests.csproj file in the ProjectName.Tests subdirectory.

This is standard naming convention and should be followed in all projects.

After following these steps, the directory should look like this:

ProjectName.Solution
├── ProjectName
│   └── ProjectName.csproj
└── ProjectName.Tests
    └── ProjectName.Tests.csproj

.csproj Files

Our .csproj files list all outside packages or dependencies a project requires. In this section, our .csproj files will contain the following:

ProjectName.Solution/ProjectName/ProjectName.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <OutputType>Exe</OutputType> // This is only needed if your project has a user interface. Read more below.
  </PropertyGroup>

</Project>
ProjectName.Solution/ProjectName.Tests/ProjectName.Tests.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
    <PackageReference Include="MSTest.TestAdapter" Version="1.3.2" />
    <PackageReference Include="MSTest.TestFramework" Version="1.3.2" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\ProjectName\ProjectName.csproj" />  
  </ItemGroup>

</Project>

Keep in mind that your .csproj should only include an OutputType if you have a user interface consisting of a Program.cs file with a Main method. When you include an OutputType before you have a user interface, attempting to compile your project to test your classes will throw an error.

Installing Packages

We use the NuGet package manager to install the packages listed in our .csproj files. For now, only the Test project's .csproj contains outside packages.

  • Navigate to the ProjectName.Tests directory.
  • Run the command $ dotnet restore.

This will create obj directories in both production and test projects. Do not touch this code.

ProjectName.Solution
├── ProjectName
│   ├── ProjectName.csproj
│   └── obj
│       ├── ProjectName.csproj.nuget.cache
│       ├── ProjectName.csproj.nuget.g.props
│       ├── ProjectName.csproj.nuget.g.targets
│       └── project.assets.json
└── ProjectName.Tests
    ├── ProjectName.Tests.csproj
    └── obj
        ├── ProjectName.Tests.csproj.nuget.cache
        ├── ProjectName.Tests.csproj.nuget.g.props
        ├── ProjectName.Tests.csproj.nuget.g.targets
        └── project.assets.json

Application Logic

Next we create the model files where our application source code will reside.

  • In the production project (ProjectName.Solution/ProjectName), create a Models subdirectory.
  • In the Models subdirectory, create a file to contain the class relevant to your project.

The resulting file structure should look like this:

ProjectName.Solution
├── ProjectName
│   ├── ProjectName.csproj
│   ├── Models
│   │   └── ClassName.cs
│   └── obj
│       └── (omitted for brevity)
└── ProjectName.Tests
    ├── ProjectName.Tests.csproj
    └── obj
        └── (omitted for brevity)

In this class file, make sure the namespace matches the name of your project (the equivalent of ProjectName). For instance:

ProjectName.Solution/ProjectName/Models/ClassName.cs
namespace ProjectName
{
  public class ClassName
  {
    // properties, constructors, methods, etc. go here
  }
}

Test Files

Create a file for tests.

  • In ProjectName.Solution/ProjectName.Tests, create a ModelTests directory.
  • In the ModelTests directory, create a test file whose name matches the class file you created in the last step. For example, if we have a Puppy.cs file in our ProjectName/Models directory that contains a Puppy class, this file should be called PuppyTests.cs. Note that the file naming convention is to pluralize Tests.
ProjectName.Solution
├── ProjectName
│   ├── ProjectName.csproj
│   ├── Models
│   │   └── ClassName.cs
│   └── obj
│        └── (omitted for brevity)
└── ProjectName.Tests
    ├── ProjectName.Tests.csproj
    ├── ModelTests
    │   └── ClassNameTests.cs
    └── obj
        └── (omitted for brevity)

This test file will have the following basic structure:

ProjectName.Solution/ProjectName.Tests/ModelTests/ClassNameTests.cs
using Microsoft.VisualStudio.TestTools.UnitTesting;
using ProjectName;

namespace ProjectName.Tests
{
  [TestClass]
  public class ClassNameTests
  {
    // Test methods go here
  }
}

Notice the instances of ProjectName and ClassName in the boilerplate code above. Like all other instances of ProjectName or ClassName in this lesson, these must be changed to match the name of your specific project.

Testing


Writing Tests

Test methods follow this general structure:

...

[TestMethod]
public void NameOfMethodWeAreTesting_DescriptionOfBehavior_ExpectedReturnValue()
{
  // any necessary logic to prep for test; instantiating new classes, etc.
  Assert.AreEqual(EXPECTED RESULT, CODE TO TEST);
}

...

In a future lesson, we'll cover other methods besides AreEqual() we can use for testing.

Running Tests

We can run our tests by navigating to our test project in the terminal (ProjectName.Solution/ProjectName.Tests) and running the following command:

$ dotnet test

This will print a report of our tests and whether they pass or fail. Any failing tests will include details about their failure.

Continued Development

Once the project is configured using the steps above, continue using the TDD workflow to build out your logic.

Lesson 8 of 20
Last updated April 14, 2022