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
102: Test-Driven Development in C#
If you ask a few developers about Test-Driven Development, you'll likely hear something like, "Oh, it's just writing your unit tests before you write the actual logic." On the surface, that sounds correct. But if that's how you view TDD, you're treating it like a chore—a bureaucratic step you have to complete before you're "allowed" to actually code. I've seen countless engineers approach it this way, and they usually end up hating it because it feels like they're doing double the work.
The Myth: TDD is a testing strategy to find bugs early
When you treat TDD as "just testing," you're still thinking about the code as the primary artifact and the test as the validation. In a traditional "test-after" workflow, you might spend three hours building a complex OrderProcessor class with five different private helper methods and a deep dependency on a database. Then, when you go to write the tests, you realize your class is a nightmare to instantiate because it's too tightly coupled. You spend another two hours trying to "force" the tests to work, often by making methods public just so you can reach them.
The bug isn't in the logic; the bug is in the design. By the time you wrote the test, the design was already baked in, and it was a bad one.
The Reality: TDD is a design tool for lean code
TDD isn't about testing; it's about specification. When I use TDD, I'm using the test to act as my first client. I'm asking myself: "If I were using this class in another part of the system, what is the simplest possible way I'd want to call it?"
Let's look at a real scenario. Imagine we need a DiscountCalculator that gives a 10% discount if the customer is a "VIP" and the order is over $100. Instead of building the whole engine, we start with the smallest possible requirement.
Step 1: Red. We write a test for a non-VIP customer. The code won't even compile yet because the class doesn't exist. That's your first "fail."
[Fact]
public void CalculateDiscount_NonVip_ReturnsZero()
{
var calculator = new DiscountCalculator();
var result = calculator.GetDiscount(100m, isVip: false);
Assert.Equal(0m, result);
}
Step 2: Green. Now, I write the absolute minimum amount of code to make that test pass. I'm not thinking about the VIP logic yet. I'm not thinking about database lookups. I'm just trying to kill the red light.
public class DiscountCalculator
{
public decimal GetDiscount(decimal amount, bool isVip) => 0m;
}
It feels like cheating, right? But this is where the magic happens. I've just defined the method signature and the return type without over-engineering. Now I move to the next requirement: the VIP discount.
[Fact]
public void CalculateDiscount_VipOver100_ReturnsTenPercent()
{
var calculator = new DiscountCalculator();
var result = calculator.GetDiscount(150m, isVip: true);
Assert.Equal(15m, result);
}
Now my first test still passes, but this one fails. I update the logic:
public decimal GetDiscount(decimal amount, bool isVip)
{
if (isVip && amount > 100m) return amount * 0.10m;
return 0m;
}
Step 3: Refactor. This is the step most people skip. Now that I have a safety net of passing tests, I can clean up the code. Maybe I want to extract the 0.10m into a constant named VipDiscountRate. I can do that with total confidence, knowing that if I break the logic, the tests will scream immediately.
Designing against the "What" instead of the "How"
The biggest shift for you will be resisting the urge to think about how the code will work. When you write the test first, you are forced to focus on what the code should achieve. This naturally leads to smaller classes and cleaner interfaces. If a test is hard to write, it's usually a signal that your design is too complex. In the "test-after" world, you just struggle through the test. In TDD, you stop and change the design because the test told you it was wrong before you spent a week building it.
📋 Practical Task
Exercise: Building a Loyalty Point Accrual Engine
You need to implement a LoyaltyPointEngine that calculates how many points a customer earns based on their spend. You are required to use a strict TDD workflow (Red-Green-Refactor). Do not write the implementation until you have a failing test.
Requirements:
- Customers earn 1 point for every $1 spent.
- If the customer has a "Gold" membership status, they earn 2 points for every $1 spent.
- Points must always be rounded down to the nearest whole integer.
Your Task:
- Create a test project and a
LoyaltyPointEngineclass. - Write a failing test for a standard member spending $10.50 (Expected: 10 points).
- Implement the minimum code to pass.
- Write a failing test for a Gold member spending $10.50 (Expected: 21 points).
- Implement the minimum code to pass.
- Refactor your code to remove any magic numbers (like the multipliers) and ensure the logic is clean.
There are no comments for now.