🏗️ Building Strong Code: Testing & Dependency Injection
Imagine you’re building with LEGO blocks. Each block does one job, and they snap together perfectly. That’s exactly how great C# code works!
🎯 The Big Picture
Think of your code like a restaurant kitchen:
- Dependency Injection (DI) = The waiter brings ingredients TO the chef (instead of the chef finding them)
- Unit Testing = Tasting each dish BEFORE serving to customers
By the end of this guide, you’ll build code that’s:
- ✅ Easy to test
- ✅ Easy to change
- ✅ Easy to understand
📦 What is Dependency Injection?
The Problem: Tight Coupling
Imagine a chef who grows their own vegetables, raises chickens, and mills flour. That chef can ONLY cook what they can produce!
// ❌ BAD: Chef makes their own ingredients
public class Chef
{
private Oven _oven = new Oven(); // Stuck with THIS oven forever!
public void Cook()
{
_oven.Bake();
}
}
What’s wrong? The Chef is GLUED to that specific Oven. Want a different oven? You’d have to rebuild the whole Chef!
The Solution: Dependency Injection
Now imagine a chef who says: “Just GIVE me an oven, and I’ll cook!”
// ✅ GOOD: Someone GIVES the chef an oven
public class Chef
{
private IIOven _oven; // Any oven will do!
public Chef(IOven oven) // Oven is INJECTED
{
_oven = oven;
}
public void Cook()
{
_oven.Bake();
}
}
Magic happens:
- Want a gas oven? Pass a
GasOven - Want an electric oven? Pass an
ElectricOven - Want a fake oven for testing? Pass a
MockOven
Three Ways to Inject Dependencies
graph TD A["🎁 Dependency Injection"] --> B["🏗️ Constructor Injection"] A --> C["📝 Property Injection"] A --> D["⚙️ Method Injection"] B --> E["Chef#40;IOven oven#41;"] C --> F["public IOven Oven { set; }"] D --> G["Cook#40;IOven oven#41;"]
1️⃣ Constructor Injection (Most Common!)
public class EmailService
{
private readonly ILogger _logger;
// Dependency comes through constructor
public EmailService(ILogger logger)
{
_logger = logger;
}
}
2️⃣ Property Injection
public class EmailService
{
// Dependency set through property
public ILogger Logger { get; set; }
}
3️⃣ Method Injection
public class EmailService
{
// Dependency passed to specific method
public void Send(ILogger logger)
{
logger.Log("Sending...");
}
}
⏰ DI Lifetimes: How Long Do Objects Live?
Imagine a coffee shop:
graph TD A["☕ DI Lifetimes"] --> B["🎪 Transient"] A --> C["🎫 Scoped"] A --> D["👑 Singleton"] B --> E["New cup every order"] C --> F["Same cup per customer visit"] D --> G["ONE cup for entire shop"]
🎪 Transient: Fresh Every Time
Like ordering a new coffee cup for EVERY sip!
services.AddTransient<ICoffee, Coffee>();
// Every time you ask, you get a NEW Coffee
var coffee1 = provider.GetService<ICoffee>();
var coffee2 = provider.GetService<ICoffee>();
// coffee1 ≠ coffee2 (different objects!)
Use when: Object is cheap to create, no shared state needed.
🎫 Scoped: Once Per Request
Like getting ONE coffee cup when you sit down, using it for your whole visit.
services.AddScoped<ICustomerOrder, CustomerOrder>();
// Same order throughout ONE web request
// New request = New order
Use when: Need same data across one HTTP request (like database contexts).
👑 Singleton: One For All
Like the shop having ONE coffee machine that everyone shares.
services.AddSingleton<ICoffeeMachine, CoffeeMachine>();
// ONE instance for the ENTIRE app lifetime
// Everyone gets the SAME machine
Use when: Expensive to create, safe to share (like configuration, caching).
Quick Comparison
| Lifetime | Created | Example |
|---|---|---|
| Transient | Every request | Email message |
| Scoped | Once per HTTP request | Database context |
| Singleton | Once ever | Configuration |
🧪 Unit Testing Basics
What IS Unit Testing?
Imagine you’re making a sandwich:
- Unit test = Check if the bread is fresh (just the bread!)
- Integration test = Check if the whole sandwich tastes good
- End-to-end test = Have someone eat the sandwich and ask them
graph TD A["🧪 Testing Pyramid"] --> B["🔬 Unit Tests"] A --> C["🔗 Integration Tests"] A --> D["🎯 E2E Tests"] B --> E["Many, Fast, Isolated"] C --> F["Some, Slower"] D --> G["Few, Slowest"]
Your First Unit Test
Let’s test a simple calculator:
// The code we want to test
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
}
// The test
[TestClass]
public class CalculatorTests
{
[TestMethod]
public void Add_TwoNumbers_ReturnsSum()
{
// Arrange (Setup)
var calc = new Calculator();
// Act (Do the thing)
int result = calc.Add(2, 3);
// Assert (Check it worked)
Assert.AreEqual(5, result);
}
}
The AAA Pattern
Every good test follows Arrange-Act-Assert:
graph LR A["🎬 Arrange"] --> B["🎯 Act"] --> C["✅ Assert"] A --> D["Setup objects"] B --> E["Call the method"] C --> F["Check the result"]
Think of it like cooking:
- Arrange = Get your ingredients ready
- Act = Cook the dish
- Assert = Taste to make sure it’s good!
🏷️ Test Attributes: Labeling Your Tests
Test frameworks use special labels (attributes) to know what’s a test.
MSTest Attributes
[TestClass] // "This class contains tests"
public class MyTests
{
[TestMethod] // "This is a test!"
public void Test1() { }
[TestInitialize] // Runs BEFORE each test
public void Setup() { }
[TestCleanup] // Runs AFTER each test
public void Cleanup() { }
[DataRow(1, 2, 3)] // Test with different data
[DataRow(0, 0, 0)]
[DataTestMethod]
public void Add_WithData_Works(int a, int b, int expected)
{
Assert.AreEqual(expected, a + b);
}
}
xUnit Attributes (Popular Alternative)
public class MyTests // No class attribute needed!
{
[Fact] // Simple test
public void Test1() { }
[Theory] // Test with data
[InlineData(1, 2, 3)]
[InlineData(0, 0, 0)]
public void Add_Works(int a, int b, int expected)
{
Assert.Equal(expected, a + b);
}
}
✅ Assertions: Checking Your Work
Assertions are like a teacher checking your homework!
Common Assertions
// Check equality
Assert.AreEqual(expected, actual);
Assert.AreNotEqual(5, result);
// Check true/false
Assert.IsTrue(user.IsActive);
Assert.IsFalse(list.IsEmpty);
// Check for null
Assert.IsNull(emptyResult);
Assert.IsNotNull(createdUser);
// Check types
Assert.IsInstanceOfType(obj, typeof(Dog));
// Check collections
CollectionAssert.Contains(list, item);
CollectionAssert.AreEqual(expected, actual);
// Check exceptions
Assert.ThrowsException<ArgumentException>(
() => calculator.Divide(1, 0)
);
Assertion Messages
Always add helpful messages!
Assert.AreEqual(
100,
account.Balance,
"Balance should be 100 after deposit"
);
🎭 Putting It All Together
Here’s a real-world example combining DI and Testing:
// 1. Define an interface
public interface IEmailSender
{
void Send(string to, string message);
}
// 2. Real implementation
public class EmailSender : IEmailSender
{
public void Send(string to, string msg)
{
// Actually sends email
}
}
// 3. Class that USES email sender (via DI)
public class OrderService
{
private readonly IEmailSender _sender;
public OrderService(IEmailSender sender)
{
_sender = sender; // Injected!
}
public void PlaceOrder(Order order)
{
// ... process order ...
_sender.Send(order.Email, "Order placed!");
}
}
// 4. Test with FAKE email sender
[TestClass]
public class OrderServiceTests
{
[TestMethod]
public void PlaceOrder_SendsEmail()
{
// Arrange - use a FAKE sender
var fakeSender = new FakeEmailSender();
var service = new OrderService(fakeSender);
var order = new Order { Email = "test@test.com" };
// Act
service.PlaceOrder(order);
// Assert
Assert.IsTrue(fakeSender.WasCalled);
}
}
public class FakeEmailSender : IEmailSender
{
public bool WasCalled { get; private set; }
public void Send(string to, string msg)
{
WasCalled = true; // Just record it was called
}
}
🚀 Key Takeaways
| Concept | Remember This |
|---|---|
| DI Basics | Don’t CREATE dependencies, RECEIVE them |
| Transient | New object every time |
| Scoped | Same object per request |
| Singleton | One object forever |
| Unit Testing | Test ONE thing in isolation |
| AAA Pattern | Arrange → Act → Assert |
| Assertions | Your code’s spell-checker |
🎉 You Did It!
You now understand:
- ✅ Why we inject dependencies (flexibility!)
- ✅ How long DI objects live (lifetimes)
- ✅ How to write unit tests (AAA pattern)
- ✅ How to use test attributes and assertions
Your code will be:
- Easier to test 🧪
- Easier to change 🔄
- Easier to understand 📖
Now go build something amazing! 🚀
