dotnet watch: you use it for hot reload. You should do more with it.

dotnet watch: beyond hot reload

dotnet watch: you use it for hot reload. You should do more with it.

Every time I changed code, I’d Alt-Tab to the terminal, arrow up, Enter. dotnet test. Wait. Check. Back to the editor. Repeat.

It’s not the end of the world. But over a full day, it adds up, mostly in lost focus. The annoying part is that I already had dotnet watch in my workflow for hot-reloading a web app. I just never thought to ask what else it could do.

Spoiler: a lot.

dotnet watch test

The most underrated command in the .NET CLI:

dotnet watch test

That’s it. Every time a .cs file changes in the project, your tests re-run automatically. No VS Code extension, no plugin, no NCrunch. Just the SDK.

It supports --filter too, so you can target a subset of tests:

dotnet watch test --filter "FullyQualifiedName~MyNamespace.MyClass"

Or by trait:

dotnet watch test --filter "Category=Unit"

While you code, only unit tests run. Integration tests stay for CI or a manual run before you commit.

Watching non-.cs files

By default, dotnet watch monitors project files: .cs, .razor, .cshtml, etc. But sometimes you need it to pick up other changes. A JSON config file, .sql files, templates.

You can add extra watch items in your .csproj:

<ItemGroup>
  <Watch Include="**\*.json" Exclude="bin\**;obj\**" />
  <Watch Include="TestData\**\*" />
</ItemGroup>

Now if I edit a test data file in TestData/, tests re-run. Handy when your tests read fixtures from disk.

A concrete example

Here’s a setup I use regularly. An xUnit project with traits to separate unit tests from integration tests.

The trait is a simple attribute:

public class CategoryAttribute : TraitAttribute
{
    public CategoryAttribute(string category)
        : base("Category", category) { }
}

Note: CategoryAttribute is used as [Category(...)] because C# lets you drop the Attribute suffix. If you prefer to keep things simple, just use [Trait("Category", "Unit")] directly on your tests.

A unit test:

[Fact]
[Category("Unit")]
public void ParseConfig_WithValidJson_ReturnsExpected()
{
    var json = """{"retries": 3, "timeout": 30}""";
    var config = ConfigParser.Parse(json);

    Assert.Equal(3, config.Retries);
    Assert.Equal(30, config.Timeout);
}

An integration test:

[Fact]
[Category("Integration")]
public void GetUser_FromDatabase_ReturnsValidUser()
{
    // ... DB access, slower
}

And the command running in the background while I code:

dotnet watch test --project tests/MyProject.Tests \
  --filter "Category=Unit"

I save a file, unit tests pass in 2 seconds. Near-instant feedback.

Bonus: split terminal

Here’s what I do most days: split my terminal in two.

  • Left pane: dotnet watch test --filter "Category=Unit", tests running continuously.
  • Right pane: dotnet watch run, the app reloading on every change.

In VS Code, Ctrl+\ splits the terminal. Windows Terminal and iTerm2 support it natively too.

The result: I save a file, and my tests run while the app reloads. At the same time. Zero friction.

┌──────────────────────────┬──────────────────────┐
│ $ dotnet watch test      │ $ dotnet watch run   │
│   --filter Category=Unit │                      │
│                          │                      │
│ Tests passed: 14         │ Now listening on:    │
│ Duration: 1.8s           │ https://localhost:5001│
│ Waiting for changes…     │ Waiting for changes… │
└──────────────────────────┴──────────────────────┘

One last thing

If dotnet watch detects a change that can’t be hot-reloaded (a method signature change, for example), it rebuilds automatically. No need to restart it.

And if you want to force a full rebuild on every change instead of hot reload:

dotnet watch test --no-hot-reload

This has made my life easier than I expected. If you’re still doing Alt-Tab, arrow up, Enter, try it for a day. You won’t want to go back.

Happy watching (and let go of the up arrow).


This post was written with the help of AI.


See also