Moq
mocking frameworks
unit testing
strict vs loose
C#

Moq, strict vs loose usage

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

In Moq, Loose and Strict control how a mock reacts to calls you did not configure explicitly. Loose returns default values and lets the test continue. Strict throws as soon as code under test touches an unexpected member. The right choice depends on whether the test is mainly about the final output or about enforcing an interaction contract.

Loose Behavior Is Tolerant by Design

Loose mode is the default because it keeps many tests lightweight. If a method was not set up, Moq supplies a default value instead of failing immediately.

csharp
1using Moq;
2
3public interface IClock
4{
5    int Hour();
6}
7
8var loose = new Mock<IClock>(MockBehavior.Loose);
9int hour = loose.Object.Hour();
10
11Console.WriteLine(hour); // default int value

This is useful when the collaborator details are not the main point of the test and the mock exists only to keep the object graph simple.

Strict Behavior Fails Fast on Unexpected Calls

Strict mode is more defensive. Every interaction the code under test performs must be set up in advance, or the test fails immediately.

csharp
1using Moq;
2
3public interface IClock
4{
5    int Hour();
6}
7
8var strict = new Mock<IClock>(MockBehavior.Strict);
9strict.Setup(c => c.Hour()).Returns(10);
10
11Console.WriteLine(strict.Object.Hour());

That makes strict mocks useful when calling the collaborator in the right way is part of the behavior being tested.

Use Loose Mode for Output-Focused Tests

If the test mainly cares about the observable result and not every collaborator interaction, loose mode is often simpler.

csharp
1using Moq;
2using Xunit;
3
4public interface IUserRepo
5{
6    string? FindName(string id);
7}
8
9public sealed class UserService
10{
11    private readonly IUserRepo _repo;
12
13    public UserService(IUserRepo repo)
14    {
15        _repo = repo;
16    }
17
18    public string DisplayName(string id)
19    {
20        return _repo.FindName(id) ?? "unknown";
21    }
22}
23
24public class UserServiceTests
25{
26    [Fact]
27    public void DisplayName_ReturnsFallbackWhenNoData()
28    {
29        var repo = new Mock<IUserRepo>(MockBehavior.Loose);
30        var sut = new UserService(repo.Object);
31
32        Assert.Equal("unknown", sut.DisplayName("u-1"));
33    }
34}

Here, strict setup would add noise without improving the test’s value much.

Use Strict Mode When Interaction Is the Contract

Strict mode becomes useful when the test exists specifically to verify that an interaction happened and happened correctly.

csharp
1using Moq;
2using Xunit;
3
4public interface IAuditWriter
5{
6    void Write(string message);
7}
8
9public sealed class LoginService
10{
11    private readonly IAuditWriter _audit;
12
13    public LoginService(IAuditWriter audit)
14    {
15        _audit = audit;
16    }
17
18    public void LoginFailed(string user)
19    {
20        _audit.Write($"failed:{user}");
21    }
22}
23
24public class LoginServiceTests
25{
26    [Fact]
27    public void LoginFailed_WritesAuditMessage()
28    {
29        var audit = new Mock<IAuditWriter>(MockBehavior.Strict);
30        audit.Setup(a => a.Write("failed:alice"));
31
32        var sut = new LoginService(audit.Object);
33        sut.LoginFailed("alice");
34
35        audit.Verify(a => a.Write("failed:alice"), Times.Once);
36    }
37}

If the service makes the wrong call, the test fails immediately instead of silently accepting a default.

A Hybrid Strategy Usually Works Best

Most test suites should not choose one behavior globally. A more practical rule is:

  • default to loose for low-risk collaborators,
  • use strict for important side-effect boundaries such as auditing, payment, messaging, or security decisions,
  • and verify only the interactions that matter to the business behavior.

That avoids the two bad extremes: brittle strict-everywhere tests and overly permissive loose-everywhere tests.

Async Tests Still Need the Same Discipline

With async methods, the main rule is to await the code under test before verification.

csharp
1using System.Threading.Tasks;
2using Moq;
3using Xunit;
4
5public interface INotifier
6{
7    Task SendAsync(string message);
8}
9
10public sealed class JobRunner
11{
12    private readonly INotifier _notifier;
13
14    public JobRunner(INotifier notifier)
15    {
16        _notifier = notifier;
17    }
18
19    public async Task RunAsync()
20    {
21        await _notifier.SendAsync("done");
22    }
23}

The strict-versus-loose decision does not change, but premature verification is a common source of flaky tests.

Common Pitfalls

  • Setting every mock to strict and making tests fail on harmless internal refactors.
  • Leaving everything loose and missing important unexpected calls.
  • Verifying incidental details that are not part of the behavior under test.
  • Using strict mocks without clear setups and then blaming Moq for test fragility.
  • Verifying async interactions before awaited work has actually completed.

Summary

  • 'Loose mode returns defaults for unconfigured calls and is good for output-focused tests.'
  • 'Strict mode fails on unexpected calls and is good for interaction-contract tests.'
  • Most test suites benefit from a mixed strategy rather than one universal rule.
  • Verify only interactions that matter to correctness.
  • Keep async tests properly awaited before verification.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.