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: Jump from fundamentals to the sample project when you want to see real code.
// Example navigation flow
// Read intro -> review AAA -> attempt practice questions

Real-world use case: Quickly revisiting the best practices section before code reviews.

Beginner Tip: Use the ToC to keep a steady learning rhythm.
Advanced Note: Share anchor links with teammates when discussing specific topics.
  1. Unit Testing Fundamentals
  2. Arrange-Act-Assert
  3. Assertions & Outcomes
  4. Test Doubles & Mocking
  5. Coverage & Boundaries
  6. Best Practices
  7. Step-by-Step Setup
  8. Sample Project
  9. FAQs
  10. Interview Questions
  11. MCQ Quiz
  12. Summary
  13. Downloads
  14. Glossary
  15. Practice

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
Example: Testing a discount calculator method with specific inputs and expected totals.
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.

Beginner Tip: Start by testing pure functions with no I/O to build confidence quickly.
Advanced Note: Keep units small by isolating dependencies via interfaces and injecting them.
Try It Yourself Placeholders: #prev-subtopic | #next-subtopic

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
Example: Arrange a cart with items, act by calculating total, assert the tax amount.
[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.

Beginner Tip: Use clear comments like Arrange/Act/Assert to guide readers.
Advanced Note: Prefer a single assert when possible, or use grouped assertions when supported.
Try It Yourself Placeholders: #prev-subtopic | #next-subtopic

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 ?
Example: Verify an API response mapper sets status to "Active".
[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.

Beginner Tip: Assert on the smallest observable output rather than internals.
Advanced Note: Use constraint-based assertions for complex objects to avoid noisy equality code.
Try It Yourself Placeholders: #prev-subtopic | #next-subtopic

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
Example: Mock a payment gateway to simulate approval and rejection paths.
[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.

Beginner Tip: Start with fakes for repositories before adding complex mocks.
Advanced Note: Use strict mocks to catch unexpected calls in critical logic.
Try It Yourself Placeholders: #prev-subtopic | #next-subtopic

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
Example: Test coupon rules at 0%, 100%, and invalid percentages.
BoundaryExample InputExpected
Minimum0 itemsNo discount
Typical3 itemsStandard discount
Maximum100 itemsBulk 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.

Beginner Tip: Always test the smallest and largest values your API accepts.
Advanced Note: Pair coverage with mutation testing to validate assertion strength.
Try It Yourself Placeholders: #prev-subtopic | #next-subtopic

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
Example: Rename "Test1" to "CalculateTax_WhenZero_ReturnsZero".
PracticeBenefit
One assertion focusClear intent
Arrange test data buildersLess duplication
Independent testsReliable 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.

Beginner Tip: If a test fails randomly, it likely shares state or relies on time.
Advanced Note: Anti-patterns like testing implementation details make refactoring painful.
Try It Yourself Placeholders: #prev-subtopic | #next-subtopic

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
Example: Step list for a .NET solution with app + tests.
  1. Create a test project named MyApp.Tests.
  2. Reference the main project in the test project.
  3. Install a unit testing package.
  4. 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.

Beginner Tip: Run tests after every small change to catch issues early.
Advanced Note: Parallelize test execution once your suite grows large.
Try It Yourself Placeholders: #prev-subtopic | #next-subtopic

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
Example: Given 10 items, discount should be 5%.
[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.

Beginner Tip: Start with the simplest rule and add scenarios incrementally.
Advanced Note: Keep shared test data in builders to avoid cross-test coupling.
Try It Yourself Placeholders: #prev-subtopic | #next-subtopic

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
Example: Q: "Should I test private methods?" A: "Test via public behavior."
// 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.

Beginner Tip: Use simple language in FAQs to reduce confusion.
Advanced Note: Revisit FAQs after incidents to address recurring misunderstandings.
Try It Yourself Placeholders: #prev-subtopic | #next-subtopic

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
Example: "Explain AAA and why it improves test readability."
  • 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.

Beginner Tip: Relate answers to a real bug you caught with tests.
Advanced Note: Discuss trade-offs like mock-heavy tests vs. in-memory fakes.
Try It Yourself Placeholders: #prev-subtopic | #next-subtopic

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
Example: Q: Which step verifies expected outcome? A: Assert.
  1. Which is the correct order in AAA? (Arrange, Act, Assert)
  2. What does a mock primarily verify? (Interactions)
  3. What should a unit test avoid? (External I/O)
  4. What is a test case? (Single scenario)
  5. 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.

Beginner Tip: Start with 5 questions and expand once learners are comfortable.
Advanced Note: Mix scenario-based questions to test applied understanding.
Try It Yourself Placeholders: #prev-subtopic | #next-subtopic

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
Example: A stable billing suite prevents revenue-impacting bugs.
// Summary checklist
// 1) Isolated units
// 2) Clear AAA
// 3) Meaningful assertions

Real-world use case: Quick review before a major release.

Beginner Tip: Keep a testing checklist with your team to align on standards.
Advanced Note: Use CI gates to fail builds when critical tests break.
Try It Yourself Placeholders: #prev-subtopic | #next-subtopic

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
Example: Include a ready-to-run test project with fixtures and mocks.
// Placeholder links
// [Download ZIP] [Download PPT] [Download DOC]

Real-world use case: Training workshops that need offline materials.

Beginner Tip: Start with a single ZIP containing minimal files.
Advanced Note: Version downloads alongside releases so learners match code with docs.
Try It Yourself Placeholders: #prev-subtopic | #next-subtopic

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
Example: "Assertion" explains what is verified in a test.
TermTooltip Description
Unit TestingVerifies smallest code units in isolation for fast feedback and regression safety.
Test FrameworkLibrary that provides attributes, assertions, and a test runner.
Test CaseSingle test method covering one scenario with expected results.
Arrange-Act-AssertPattern to structure tests into setup, execution, and verification.
AssertCheck comparing expected and actual values to pass or fail tests.
Test DoublesStand-ins for dependencies like fakes, stubs, and mocks.
MockingCreating fake objects that track interactions and return preset data.
Dependency InjectionPassing services into a class so they can be replaced for tests.
Test RunnerTool that discovers and executes tests, reporting results.
TDDTest-driven development: write tests first, then code to pass.
CoverageMeasure of code executed by tests, helpful for finding gaps.
Test FixtureClass 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.

Beginner Tip: Hover terms while reading to build vocabulary quickly.
Advanced Note: Expand the glossary as your team adds new testing tools.
Try It Yourself Placeholders: #prev-subtopic | #next-subtopic

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
Example: Write tests for a currency conversion method with invalid inputs.

Easy (5)

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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)

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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)

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

Beginner Tip: Write the tests before the implementation to sharpen focus.
Advanced Note: Add negative tests to validate error paths.
Try It Yourself Placeholders: #prev-subtopic | #next-subtopic