Professional Documents
Culture Documents
C Unit Testing NUnit, Moq, and Beyond (Parvin, R.)
C Unit Testing NUnit, Moq, and Beyond (Parvin, R.)
Why a Pyramid?
Here’s why this pyramid structure matters:
● Speed: Unit tests are lightning-fast, providing rapid feedback as
you develop. Integration and E2E tests tend to be slower to execute.
● Cost: Unit tests are comparatively inexpensive to write and run.
The higher you go on the pyramid, the more complex, time-
consuming, and potentially brittle your tests can become.
● Focus: The pyramid encourages you to focus on testing the core
logic of your application at the unit level, where most bugs are
likely to appear.
Ideal Proportions
While the ratio of tests will vary depending on your project’s nature, the
general guideline is: Have a substantial base of unit tests, a smaller section
of integration, and a select few targeted E2E tests.
Benefits of the Test Pyramid
● Resilient Projects: A solid foundation of unit tests promotes
greater code stability over time.
● Optimized Efforts: The pyramid guides you towards a balanced
testing strategy, maximizing return on investment.
● Faster Feedback Loops: With more unit tests and their rapid
execution, you get early signals of potential problems.
Additional Resources
● Introducing the Software Testing Pyramid:
https://martinfowler.com/articles/practical-test-pyramid.html
● The Test Pyramid: A Guide to Better Automated Software
Testing: https://www.browserstack.com/guide/testing-pyramid-for-
test-automation
Beyond the Classic Pyramid
It’s important to note that the classic test pyramid is a conceptual model and
shouldn’t be treated as rigid dogma. Some projects might benefit from
slightly modified shapes. It’s about finding the best balance for your
context.
Get Ready to Dive Deep
Now that you grasp the essence of the testing pyramid, the next chapter will
take you into the world of testing tools. You’ll learn about essential tools for
writing and executing your C# tests.
Let’s move forward!
Navigating the Testing Tool Landscape
The world of C# testing offers a rich ecosystem of tools that empower you
to write, run, and analyze your automated tests. Let’s take a tour of the key
categories and popular choices to help you select the best fit for your
projects.
1. Testing Frameworks
These frameworks provide the structure and vocabulary for composing your
tests. Key players in the C# world include:
● NUnit: A mature, widely-adopted, and flexible testing framework.
We’ll focus on NUnit later in the book.
● xUnit: A modern, lean, and extensible testing framework inspired
by the fundamentals of NUnit.
● MSTest: A testing framework built into Visual Studio, providing
seamless integration with the IDE.
2. Assertion Libraries
Assertion libraries offer a clear way to express the expected results of your
tests. Popular options include:
● Built-in Assertions: NUnit, xUnit, and MSTest all come with their
own assertion mechanisms.
● Fluent Assertions: Provides a readable, fluent syntax for writing
assertions (e.g., myResult.Should().BePositive() ).
● Shouldly: Another assertion library focusing on expressive
assertions (e.g. myResult.ShouldBePositive() .
3. Mocking Frameworks
Mocking is essential for isolating your code during unit testing. A top
choice for C# is:
● Moq: Powerful, versatile, and easy to learn. Supports both state-
based and interaction-based testing styles. We’ll dive deep into Moq
in later sections.
4. Code Coverage Tools
Code coverage gives you insight into what percentage of your code is
executed by your tests. Popular tools include:
● dotCover: A powerful commercial code coverage tool from
JetBrains, with excellent Visual Studio and Rider integration.
● OpenCover: A free and open-source alternative for code coverage
analysis.
5. Test Runners
Test runners discover and execute your tests. They’re often integrated with
your IDE or build system.
● Visual Studio Test Explorer: Built-in test runner for Visual Studio.
● ReSharper Test Runner: JetBrains’ advanced test runner for
ReSharper.
● NUnit Console Runner: Command-line runner for executing
NUnit tests.
Choosing Your Toolkit
Here’s a simplified starting point that will serve you well:
● Testing Framework: NUnit
● Assertion Library: NUnit’s built-in assertions with potential
exploration of Fluent Assertions
● Mocking Framework: Moq
● IDE: Visual Studio or Rider
Don’t Get Overwhelmed
This landscape might seem vast. The key is to start with a core set of tools
and expand your knowledge as you go deeper into unit testing. We’ll focus
on using NUnit, Moq, and IDE-integrated test runners to give you a
powerful and practical testing setup.
Additional Resources
● C#/.NET Testing Tools: https://github.com/topics/testing-tools
Practical Experience Ahead
In the next chapter, you’ll take a leap forward by writing and understanding
your first C# unit test!
Let’s get those hands-on skills going!
// Scenario 1
Assert.AreEqual(-5, calculator.Add(-3, -2));
//Scenario 2
Assert.AreEqual(2, calculator.Add(5, -3));
}
Instead of separate test methods, we use multiple Assert statements for
various scenarios.
2. Parameterized Tests for Data-Driven Testing
Expand the concept above with NUnit’s parameterized tests:
[Test]
public void Add_HandlesVariousInputs_ReturnsCorrectSum(
[Values(2, 5, 8)] int a,
[Values(-4, 0, 3)] int b,
[Values(-2, 5, 11)] int expectedResult)
{
var calculator = new Calculator();
Assert.AreEqual(expectedResult, calculator.Add(a, b));
}
NUnit will run the test multiple times, each with a combination of the
provided parameters!
3. The Art of Test-Driven Development (TDD)
While not strictly a composition technique, let’s introduce TDD:
● Red: First, write a failing test for a feature that doesn’t exist yet.
● Green: Write the minimal implementation to only make the test
pass.
● Refactor: Improve your code’s design while ensuring tests still
pass.
TDD guides development with testing as its foundation. We’ll dedicate a
full chapter to it later!
4. Testing Edge Cases and Error Conditions
Don’t just test the “happy path”. Good tests explore boundaries:
[Test]
public void Add_LargeNumbers_ThrowsOverflowException()
{
var calculator = new Calculator();
Assert.Throws<OverflowException>(() => calculator.Add(int.MaxValue,
1));
}
5. Test Naming as Documentation
Descriptive names make tests self-explanatory:
[Test]
public void Divide_DenominatorZero_ThrowsDivideByZeroException()
{ ... }
6. Organizing with Setup/Teardown
For repeated setup and cleanup steps in multiple tests, use NUnit’s [SetUp]
and [TearDown] attributes on methods within your test class. Be mindful:
these can make tests less isolated if misused.
Additional Resources
● Parameterized Tests in NUnit:
https://docs.nunit.org/articles/nunit/writing-tests/parameterized-
tests.html
● NUnit Setup and Teardown:
https://docs.nunit.org/articles/nunit/writing-
tests/setuptearndown.html
Beyond the Basics
We’ve only scratched the surface here. As you get more comfortable with
unit testing, you’ll discover more patterns and tools for handling increasing
complexity.
Up Next
To make sure your tests actually aid in quality code, it’s vital to understand
what constitutes sufficient test coverage. Let’s move on to the next chapter!
Ensuring Comprehensive Test
Coverage: Beyond Unit Tests
Unit tests are your front line of defense, but achieving truly robust software
requires a broader testing strategy. Let’s explore why it’s important to go
beyond unit tests and get acquainted with some of the key testing types to
expand your safety net.
1. Why “More Tests” Isn’t Always the Answer
You might be tempted to simply aim for writing as many unit tests as
possible. However, untargeted testing is inefficient. To maximize the value
of your testing efforts, you need a multi-layered approach. Remember the
test pyramid!
2. Integration Tests: Playing Well Together
Where unit tests examine individual components in isolation, integration
tests check how multiple units collaborate:
● Example: An integration test might verify that a repository class in
your application correctly communicates with the database.
● Purpose: They uncover issues in how components communicate,
ensuring that the ‘wiring’ of your system works as intended.
3. System Tests: The Big Picture
System tests focus on the behavior of your entire application from the user’s
perspective:
● Example: Simulating a user journey through a web application,
including interactions with the user interface (UI).
● Purpose: Verifies that all the parts of your system integrate
correctly to deliver the expected user experience.
4. End-to-End (E2E) Tests: As Real as it Gets
End-to-end tests thoroughly exercise your system, often involving external
dependencies like databases, web services, or even hardware integrations:
● Example: Testing a full payment processing flow, inclusive of
interactions with an external payment gateway.
● Purpose: Provides the highest level of confidence that the system
works as a whole, replicating real-world user scenarios.
5. Other Important Test Types
Let’s briefly touch on other test types you’ll encounter:
● Performance Tests: Assess if your system meets responsiveness
and scalability targets.
● Acceptance Tests: Ensure that the software fulfills business
requirements as defined by users or stakeholders.
● Regression Tests: Designed to catch reintroduced errors, guarding
against the unintended breakages of previously working
functionality.
The Right Mix
Finding the right testing blend for your project is essential. Not every
application needs extensive E2E tests, but neglecting integration tests
altogether is rarely a good strategy.
Beyond Introduction
As you become a seasoned unit tester, you’ll gain the experience to
strategically use a combination of these test types to maximize the quality
of your software while minimizing the cost and time for testing.
Next Up
Testing and refactoring go hand-in-hand. In the next chapter, we’ll see how
tests empower you to make changes to your code with more confidence.
Empowering Refactoring Practices
with Testing
Refactoring is the art of restructuring your code to improve its readability,
design, and maintainability – all without changing its external behavior.
Automated tests, especially unit tests, are your superpower when it comes
to fearless refactoring.
Why Refactoring Matters
● Adaptable Code: As projects evolve, refactoring keeps your
codebase flexible and easier to adjust to new requirements.
● Preventing Rot: Untouched, code tends to degrade over time.
Refactoring helps fight complexity “rot” and technical debt.
● Developer Happiness: Clean, well-designed code is simply more
enjoyable to work with!
Tests: Your Refactoring Safety Net
Imagine refactoring without tests. Every change introduces a nagging fear
of accidentally breaking something. With a solid set of tests, you get this
amazing advantage:
1. Make Changes: Restructure your code as needed.
2. Run Tests: Your test suite automatically runs.
3. Instant Feedback: If tests pass, you have high confidence your
changes haven’t introduced unexpected problems. If a test fails,
it pinpoints exactly where you need to focus.
Illustrative Example
Let’s say you have a complex method CalculateOrderTotal that’s become
difficult to understand. With tests covering various CalculateOrderTotal
scenarios, you can:
● Break down the method into smaller, more focused functions.
● Introduce clearer variable names.
● Optimize the calculation logic.
…all while tests keep verifying it still gives the correct output!
Embracing Change with Confidence
Refactoring with a strong test suite in place transforms your mindset.
Instead of fearing change, you embrace it, confident that your tests will
quickly signal if you make a misstep.
Types of Refactoring
Here are common refactoring techniques where tests shine:
● Extract Method: Decouple a long method into smaller, well-named
functions.
● Rename: Improve readability with more descriptive class, method,
or variable names.
● Introduce Design Patterns: Apply established patterns (Observer,
Factory, etc.) to enhance structure and flexibility.
Additional Resources
● Refactoring.Guru (Catalog of Refactorings):
https://refactoring.guru/
● Martin Fowler: Refactoring
https://martinfowler.com/books/refactoring.html (A classic book on
refactoring)
Key Takeaway
Refactoring and testing form a powerful cycle. Tests give you the courage
to refactor, and refactoring often leads to simpler code that is easier to test.
This promotes long-term code health.
Up Next
Now that you understand the importance of testing, let’s dive into the
specifics of writing excellent unit tests using the powerful NUnit
framework.
Let’s get testing with NUnit!
Leveraging NUnit for Efficient Testing
in Visual Studio
NUnit is a battle-tested unit testing framework for C# and the .NET
ecosystem. When paired with Visual Studio, you get a seamless testing
experience. Let’s explore how to use them in tandem.
Prerequisites
● Basic Visual Studio experience (creating projects, etc.)
● Understanding of unit testing concepts from previous chapters
1. Installing NUnit
The easiest way is using Visual Studio’s NuGet Package Manager:
1. Right-click on your test project -> “Manage NuGet
Packages…”
2. Search for ‘NUnit’ and install the NUnit package.
3. Install the ’NUnit3TestAdapter* package as well. This enables
Visual Studio to discover and run your tests.
Section 2:
Mastering Unit Testing Fundamentals
Additional Resources
● JetBrains Rider Unit Testing Documentation:
https://www.jetbrains.com/help/rider/Getting_Started_with_Unit_Te
sting.html
● JetBrains Blog Posts on Testing
https://blog.jetbrains.com/dotnet/tag/testing/
Efficiency and Insight
Rider doesn’t just run your tests, it transforms unit testing into a fluid
conversation between your code and its tests. This tight integration helps
you write better tests faster and fix issues with surgical precision.
Next Up
Let’s make your work on a real-world class using Rider with hands-on
guidance. The next chapter will provide a practical walkthrough for creating
your first unit tests in Rider.
Section 3:
Advanced Unit Testing Techniques
Each of these tools has its own set of features, advantages, and limitations,
so you may want to evaluate them based on your specific requirements and
preferences.
The Right Mindset
Code coverage is a tool, not a master. Use it to:
● Uncover blind spots
● Drive the creation of new, meaningful tests
● Identify potentially unused code
Up Next
Testing is an evolving skill. Let’s discuss some real-world testing
challenges and the best practices to overcome them.
Section 4:
Decoupling Dependencies for
Testability
Additional Resources
● Martin Fowler’s Refactoring Catalog:
https://refactoring.com/catalog/ A classic.
● Book: Working Effectively with Legacy Code (Michael
Feathers): https://www.amazon.com/Working-Effectively-Legacy-
Michael-Feathers/dp/0131177052
Cautions
● Don’t Change Everything at Once: Large rewrites are risky.
Small steps are your friend.
● Tests as a Guide: Your test suite tells you when your refactoring is
functionally equivalent and when you’ve made a mistake.
Up Next
In Part 2, we’ll look at specific refactoring techniques and the core concept
of dependency injection, a powerful pattern for creating loosely coupled
code.
Step-by-Step Refactoring Towards a
Loosely-coupled Architecture, Part 2
In the previous chapter, we started introducing seams into our code. Now,
let’s learn how to fully leverage those seams to inject dependencies, making
our units gloriously testable.
1. Dependency Injection (DI) in a Nutshell
● Core Idea: A class doesn’t create its dependencies directly. Instead,
they are ‘injected’ from the outside.
● Benefits:
○ Decoupling at its finest
○ Easily swap in test doubles
○ Increased flexibility in our application’s overall structure
2. Types of Dependency Injection
● Constructor Injection: Dependencies are passed through the
constructor. (The most preferred for its clarity and testability)
● Property Injection: Dependencies are set through public
properties. (Used sometimes, but less ideal for testability)
● Method Injection: A dependency is passed directly as a parameter
to a specific method. (Useful at times, but not a whole-system DI
approach)
3. Refactoring Continued: Our Database Example
Let’s assume after Part 1 we have:
public interface ICustomerRepository { ... }
public class CustomerService
{
private readonly ICustomerRepository _repository;
public CustomerService(ICustomerRepository repository) {
_repository = repository;
}
// ... rest of the code using _repository
}
Steps
1. Real vs. Fake: Create a DatabaseCustomerRepository (real)
and maybe a FakeCustomerRepository (for tests). Both
implement ICustomerRepository .
2. Wiring it Up: Somewhere in your application’s startup or
composition logic, you’ll decide which to use:
// Production Mode
var customerService = new CustomerService(new
DatabaseCustomerRepository("..."));
// Test Mode
var customerService = new CustomerService(new
FakeCustomerRepository());
4. Taking It Further
● Manual DI vs. Frameworks: What we did was “manual”
dependency injection. For large projects, consider Dependency
Injection Frameworks which automate object ‘wiring’ based on
configuration. We’ll cover these later!
Additional Resources
● Mark Seemann on Dependency Injection: https://blog.ploeh.dk/
(Search his site for DI content, he’s a thought leader on the topic)
Trade-offs
● Increased Indirection: DI can potentially make the flow of your
application slightly harder to trace for newcomers. Good
documentation helps!
● The Right Amount: Be pragmatic. Not everything must be
injected, finding the balance is key.
Z-Access
https://wikipedia.org/wiki/Z-Library
ffi
fi