C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
97: Unit Testing with xUnit
I've seen this exact scenario play out in countless pull requests: a developer writes a piece of logic, writes one "happy path" test that passes, and assumes the feature is bulletproof. Then, the first customer with an unusual edge case hits the production server, and everything crashes.
Take a look at this DiscountCalculator. It's meant to apply a percentage discount to a shopping cart total. On the surface, it looks fine.
public class DiscountCalculator
{
public decimal ApplyDiscount(decimal total, decimal discountPercentage)
{
return total - (total * (discountPercentage / 100));
}
}
// The learner's initial test
public class DiscountTests
{
[Fact]
public void ApplyDiscount_ShouldReducePrice()
{
var calculator = new DiscountCalculator();
var result = calculator.ApplyDiscount(100m, 20m);
Assert.Equal(80m, result);
}
}
The Hidden Negative Balance Bug
The test above passes perfectly. But here is where the real world bites: what happens if a marketing manager creates a "110% off" coupon as a mistake? Or what if the discountPercentage passed in is a negative number?
If you pass 110m as the discount, the method returns -10m. You've just created a system where the store pays the customer to take the product. If you pass -20m, the price actually increases. Neither of these behaviors is acceptable, and our single [Fact]` test didn't catch it because it only tested the "happy path."
Hardening Logic with Theory and InlineData
In xUnit, a [Fact] is for a test that is always true. But when you're dealing with calculations, you usually want to test a range of inputs. That's where [Theory] comes in. Instead of writing five different test methods, you can use [InlineData] to feed multiple scenarios into a single test method.
Here is how I would rewrite this to catch those bugs and fix the underlying logic:
public class DiscountCalculator
{
public decimal ApplyDiscount(decimal total, decimal discountPercentage)
{
// We clamp the discount between 0% and 100% to prevent negative prices
// or accidental price hikes.
var sanitizedDiscount = Math.Clamp(discountPercentage, 0, 100);
return total - (total * (sanitizedDiscount / 100));
}
}
public class DiscountTests
{
[Theory]
[InlineData(100, 20, 80)] // Standard case
[InlineData(100, 0, 100)] // No discount
[InlineData(100, 100, 0)] // Free item
[InlineData(100, 110, 0)] // Over 100% should be treated as free (0)
[InlineData(100, -20, 100)] // Negative discount should be treated as 0%
public void ApplyDiscount_ShouldHandleEdgeCases(decimal total, decimal discount, decimal expected)
{
var calculator = new DiscountCalculator();
var result = calculator.ApplyDiscount(total, discount);
Assert.Equal(expected, result);
}
}
By switching to a [Theory], we've turned a fragile test into a specification. The Math.Clamp call ensures that no matter what garbage data gets passed into the method, the result stays within a logical business range. This is the core philosophy of unit testing: don't just prove that your code works; try your hardest to prove that it can't break.
Dealing with Floating Point Precision
A quick side note: you'll notice I used decimal here. If you're writing tests for financial data and you use double or float, Assert.Equal will eventually fail you. Why? Because of binary floating-point inaccuracies (e.g., 0.1 + 0.2 isn't exactly 0.3).
If you absolutely must use doubles, xUnit provides an overload for Assert.Equal that allows for a "precision" or "tolerance" level:
// Asserts that the values are equal within 3 decimal places
Assert.Equal(expectedDouble, actualDouble, 3);
I personally avoid doubles for money entirely, but knowing this overload is a lifesaver when you're testing scientific calculations or coordinates.
📋 Practical Task
Exercise: Building a Bulletproof Password Validator Test Suite
You have been handed a PasswordValidator class that is currently too permissive. Your goal is to implement the validation logic and write a comprehensive xUnit test suite to ensure it cannot be bypassed.
The Requirements:
- Password must be at least 8 characters long.
- Password must contain at least one digit.
- Password must contain at least one uppercase letter.
- If the password contains the word "Password" (case-insensitive), it must be rejected regardless of other rules.
Your Task:
- Implement the
IsValid(string password)method in thePasswordValidatorclass. - Create a test class using
[Theory]and[InlineData]. - Add at least 6 test cases: 3 that should return
trueand 3 that should returnfalse(covering different failure reasons like length, missing digits, or the forbidden word).
public class PasswordValidator
{
public bool IsValid(string password)
{
// TODO: Implement logic here
}
}
// TODO: Create your xUnit test class here
There are no comments for now.