๐๏ธ 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! ๐
