Unit Testing in C#
Table of Contents
This table mirrors the sidebar subtopics and helps you jump to any section quickly. It acts as your quick navigation map while learning unit testing. Use it to revisit concepts like assertions, mocks, and coverage without scrolling. A good ToC also reveals the structure of the learning path.
Start -> Pick Section -> Learn -> Practice
// Example navigation flow
// Read intro -> review AAA -> attempt practice questions
Real-world use case: Quickly revisiting the best practices section before code reviews.
1. Unit Testing Fundamentals
Test frameworks make small, repeatable tests that validate behavior one method at a time. A test case is quick to run, easy to read, and focused on a single expectation. Unit tests reduce bugs early and support safe refactoring. They are different from integration tests because they avoid external dependencies. Clean unit tests are deterministic, fast, and act as living documentation.
Code Change
|
v
Unit Test ----> Pass/Fail ----> Confidence
|
v
Refactor Safely
public int ApplyDiscount(int total, int percent)
{
if (percent < 0) throw new ArgumentOutOfRangeException(nameof(percent));
return total - (total * percent / 100);
}
// Unit test pseudo-check
Assert.AreEqual(90, ApplyDiscount(100, 10));
Real-world use case: Ensuring a billing rule stays correct when pricing logic changes.
2. Arrange-Act-Assert (AAA)
AAA is a simple flow for writing a clear unit test. You arrange the data and mocks, act by calling the method, then assert the expected result. This pattern avoids long, confusing tests by separating phases. It also highlights when a test tries to do too much.
Arrange ---> Act ---> Assert
setup execute verify
[Test]
public void Total_Includes_Tax()
{
// Arrange
var cart = new Cart(100m);
// Act
var total = cart.TotalWithTax(0.10m);
// Assert
Assert.AreEqual(110m, total);
}
Real-world use case: Validating that a shipping fee is added when weight exceeds a threshold.
3. Assertions & Outcomes
Assertions are the core of a unit test because they compare expected and actual values. A good assertion explains what behavior matters most to your business rule. Use descriptive messages so failures are easy to diagnose. Avoid over-asserting, which can make tests brittle when implementation changes. A focused test with one key expectation is easier to maintain.
Input
|
v
Method
|
v
Expected == Actual ?
[Test]
public void MapStatus_ReturnsActive()
{
var result = StatusMapper.Map("A");
Assert.AreEqual("Active", result, "Status code 'A' should map to Active");
}
Real-world use case: Catching regressions in status mapping used for customer dashboards.
4. Test Doubles & Mocking
Unit tests stay fast by replacing slow dependencies with test doubles. Dependency Injection makes it easy to swap real services with fakes. Use mocks to verify interactions, like ensuring an email sender was called. Prefer simple fakes for data access or configuration when behavior is predictable. Avoid over-mocking, which can mirror the implementation too closely.
Service ----> External API
| |
v v
Mock Service <---X
[Test]
public void Checkout_SendsEmailOnSuccess()
{
var email = new Mock<IEmailService>();
var checkout = new Checkout(email.Object);
checkout.CompleteOrder();
email.Verify(m => m.Send(It.IsAny<string>()), Times.Once);
}
Real-world use case: Testing invoice generation without calling a real SMTP server.
5. Coverage & Boundary Testing
Coverage reports show which code paths your tests executed. Boundary testing focuses on the edges: minimums, maximums, and invalid inputs. Combine both to reveal missing scenarios in business logic. High coverage is helpful, but meaningful assertions are more important than numbers. Track coverage trends to detect risk areas during refactors.
Input Range
[ min | valid | max ]
^ ^ ^
test test test
| Boundary | Example Input | Expected |
|---|---|---|
| Minimum | 0 items | No discount |
| Typical | 3 items | Standard discount |
| Maximum | 100 items | Bulk discount |
[TestCase(0, 0)]
[TestCase(3, 5)]
[TestCase(100, 20)]
public void Discount_Boundaries(int items, int expected)
{
Assert.AreEqual(expected, DiscountRules.Calculate(items));
}
Real-world use case: Preventing pricing bugs when orders spike during promotions.
6. Best Practices & Anti-Patterns
Keep tests isolated, fast, and readable. Avoid sharing state between tests because order dependence hides failures. Name tests with clear intent such as Method_WhenCondition_ShouldResult. Use small test data builders to reduce duplication. Avoid testing private methods directly; test through public behavior.
Good Test
|
+--> Isolated
+--> Deterministic
+--> Readable
| Practice | Benefit |
|---|---|
| One assertion focus | Clear intent |
| Arrange test data builders | Less duplication |
| Independent tests | Reliable runs |
[Test]
public void CalculateTax_WhenZero_ReturnsZero()
{
var tax = TaxCalculator.Calculate(0m, 0.1m);
Assert.AreEqual(0m, tax);
}
Real-world use case: Preventing flaky tests in CI pipelines.
7. Step-by-Step: Create Unit Tests in C#
A test runner loads your tests and reports pass/fail results. Start by creating a test project and referencing the production project. Add a test framework and write your first test class. Execute tests locally to confirm the setup works before expanding coverage. Repeat the loop as you add features.
Create Project
|
Add Reference
|
Write Test
|
Run Runner
- Create a test project named MyApp.Tests.
- Reference the main project in the test project.
- Install a unit testing package.
- Add a test class and run tests.
dotnet new nunit -n MyApp.Tests
cd MyApp.Tests
dotnet add reference ../MyApp/MyApp.csproj
dotnet test
Real-world use case: Adding guard tests before refactoring a legacy service.
8. Sample Project: Pricing Rules
This sample tests a pricing engine with tiered discounts. The test fixture includes tests for base price, bulk discounts, and invalid inputs. It demonstrates use of parameters and boundary checks. You can extend the project to include taxes or regional pricing rules.
Items -> PricingEngine -> FinalTotal
^ |
| v
Test Data Assertions
[TestFixture]
public class PricingEngineTests
{
[TestCase(1, 0)]
[TestCase(10, 5)]
[TestCase(100, 20)]
public void Discount_ByQuantity(int items, int expected)
{
Assert.AreEqual(expected, PricingEngine.Discount(items));
}
}
Real-world use case: Preventing incorrect discounting during seasonal campaigns.
9. FAQs
FAQs clarify common questions about unit testing scope and tooling. They help beginners differentiate between unit, integration, and end-to-end tests. Use FAQs to settle questions about speed, isolation, and test reliability. Keep answers short and link to deeper sections in your docs.
Q1? -> A1
Q2? -> A2
Q3? -> A3
// FAQ example:
// Q: Should tests call databases?
// A: Not in unit tests; use integration tests instead.
Real-world use case: Onboarding new team members into a testing culture.
10. Interview Questions
Interview questions assess your ability to explain why tests matter. They cover terms like TDD and how you isolate dependencies. Use clear answers with examples from real projects. Show how you prevent flaky tests and use CI.
Question -> Reasoning -> Example
- What is the difference between unit and integration testing?
- How do mocks help maintain test isolation?
- When would you use parameterized tests?
- How does CI benefit from fast unit tests?
// Sample response snippet
// "I use parameterized tests when the same logic must
// be validated across multiple input combinations."
Real-world use case: Preparing for senior developer interviews.
11. MCQ Quiz
Multiple-choice questions reinforce key concepts quickly. They are useful for quick self-assessment after reading the tutorial. Focus on important definitions and practices such as mocks and assertions. Add explanations for correct answers when you publish the quiz.
Question -> Options -> Answer
- Which is the correct order in AAA? (Arrange, Act, Assert)
- What does a mock primarily verify? (Interactions)
- What should a unit test avoid? (External I/O)
- What is a test case? (Single scenario)
- Why keep tests fast? (Enable frequent runs)
// Quiz grading pseudo-code
var score = answers.Count(a => a.IsCorrect);
Real-world use case: Running a quick knowledge check during onboarding.
12. Summary of Key Points
Unit testing builds confidence in small pieces of code by verifying behavior. Use AAA to structure tests and clear assertions to describe expectations. Mocks and fakes isolate dependencies, keeping tests fast and deterministic. Coverage and boundary tests highlight missing scenarios. Consistent naming and isolation reduce flaky tests in CI pipelines.
Reliable Tests -> Safer Refactors -> Stable Releases
// Summary checklist
// 1) Isolated units
// 2) Clear AAA
// 3) Meaningful assertions
Real-world use case: Quick review before a major release.
13. Downloadable Resources (ZIP, PPT, DOC)
Provide downloadable assets so learners can practice offline. A ZIP can include sample code and datasets. A PPT can summarize key concepts for classroom delivery. A DOC can include exercises and project briefs. Keep download sections updated as content evolves.
Resources
|- UnitTesting.zip
|- UnitTesting.pptx
|- UnitTesting.docx
// Placeholder links
// [Download ZIP] [Download PPT] [Download DOC]
Real-world use case: Training workshops that need offline materials.
14. Glossary (Tooltips)
This glossary lists the tooltip terms used throughout the page. Review it to reinforce your understanding of testing vocabulary. Each term matches the tooltip description shown on hover. Use it as a quick reference while writing tests.
Term -> Meaning -> Usage
| Term | Tooltip Description |
|---|---|
| Unit Testing | Verifies smallest code units in isolation for fast feedback and regression safety. |
| Test Framework | Library that provides attributes, assertions, and a test runner. |
| Test Case | Single test method covering one scenario with expected results. |
| Arrange-Act-Assert | Pattern to structure tests into setup, execution, and verification. |
| Assert | Check comparing expected and actual values to pass or fail tests. |
| Test Doubles | Stand-ins for dependencies like fakes, stubs, and mocks. |
| Mocking | Creating fake objects that track interactions and return preset data. |
| Dependency Injection | Passing services into a class so they can be replaced for tests. |
| Test Runner | Tool that discovers and executes tests, reporting results. |
| TDD | Test-driven development: write tests first, then code to pass. |
| Coverage | Measure of code executed by tests, helpful for finding gaps. |
| Test Fixture | Class grouping related tests with shared setup. |
// Glossary usage example
// "Use AAA to structure each test case."
Real-world use case: Quick reference for teams adopting a new testing standard.
To Become Best coder Practice this
Practice questions strengthen your testing skills with realistic scenarios. Each question includes inputs, outputs, constraints, and a hint. Solve them using C# unit tests and validate expected outcomes. Mix easy, medium, and hard problems to build depth.
Practice -> Implement -> Test -> Review
Easy (5)
-
Problem: Test that a temperature converter returns 32°F for 0°C.
Input/Output: Input: 0, Output: 32.
Example: In: 0 → Out: 32.
Constraints: -100 ≤ C ≤ 100.
Hint: Use the formula C * 9/5 + 32. -
Problem: Test that a string reverser handles empty strings.
Input/Output: Input: "", Output: "".
Example: In: "" → Out: "".
Constraints: Length ≤ 100.
Hint: Treat empty as a valid input. -
Problem: Test that a sum method returns 0 for an empty list.
Input/Output: Input: [], Output: 0.
Example: In: [] → Out: 0.
Constraints: List size ≤ 1000.
Hint: Initialize sum to 0. -
Problem: Test that a boolean toggle flips true to false.
Input/Output: Input: true, Output: false.
Example: In: true → Out: false.
Constraints: Boolean only.
Hint: Use logical negation. -
Problem: Test that a string length function handles null safely.
Input/Output: Input: null, Output: 0.
Example: In: null → Out: 0.
Constraints: Null allowed.
Hint: Use null-coalescing.
Medium (5)
-
Problem: Test that order totals include shipping for weights over 5kg.
Input/Output: Input: weight=6, Output: total+shipping.
Example: In: 6kg → Out: base+shipping.
Constraints: weight ≥ 0.
Hint: Boundary at 5kg. -
Problem: Test that a login method locks after 3 failed attempts.
Input/Output: Input: 3 fails, Output: locked=true.
Example: In: 3 fails → Out: locked.
Constraints: Attempts ≤ 10.
Hint: Use a counter state. -
Problem: Test that a coupon applies only to eligible categories.
Input/Output: Input: category=Electronics, Output: no discount.
Example: In: Electronics → Out: 0%.
Constraints: Category list fixed.
Hint: Use a set of allowed categories. -
Problem: Test a paging method returns correct items on page 2.
Input/Output: Input: page=2,size=5 → Output: items 6-10.
Example: In: 2,5 → Out: [6..10].
Constraints: size ≤ 50.
Hint: Skip (page-1)*size. -
Problem: Test that a validator rejects emails without '@'.
Input/Output: Input: "user.com" → Output: false.
Example: In: user.com → Out: false.
Constraints: Length ≤ 255.
Hint: Check for '@' presence.
Hard (5)
-
Problem: Test concurrency safety in a counter increment method.
Input/Output: Input: 100 parallel increments → Output: 100.
Example: In: 100 tasks → Out: 100.
Constraints: Use thread-safe operations.
Hint: Consider lock or Interlocked. -
Problem: Test a cache with expiration invalidates after 5 minutes.
Input/Output: Input: time=6 min → Output: cache miss.
Example: In: 6 min → Out: miss.
Constraints: Use a clock abstraction.
Hint: Inject a fake time provider. -
Problem: Test a retry policy stops after max attempts on failure.
Input/Output: Input: max=3, Output: 3 tries.
Example: In: 3 → Out: 3.
Constraints: Do not sleep in tests.
Hint: Mock delay service. -
Problem: Test a price calculator for overlapping discount rules.
Input/Output: Input: VIP + Bulk → Output: best discount.
Example: In: VIP+Bulk → Out: max discount.
Constraints: Only one discount applies.
Hint: Use rule priority. -
Problem: Test an event-driven handler publishes an audit record.
Input/Output: Input: event, Output: audit call once.
Example: In: event → Out: publish.
Constraints: Idempotent writes.
Hint: Mock publisher and verify invocation.
// Practice scaffolding
// Use AAA, clear assertions, and deterministic fakes.
Real-world use case: Preparing for coding assessments with a testing focus.