Day 08 : Unit Testing — The Real Reason Interfaces Exist
Proving service behavior without repositories or databases
Introduction
Many tutorials introduce interfaces and immediately move on to databases. At that point, learners often ask:
“Why did we even create this interface?”
This step answers that question. The real purpose of the interface appears when we start writing unit tests.
🧪 Step : The Proof
The reason we created IHelloRepository was not abstraction for its own sake.
It was to make one thing possible:
testing business logic in isolation.
- We want to test the behavior of
HelloService. - We do not want to touch memory, databases, or storage logic.
- We want tests that are fast, predictable, and repeatable.
This is only possible because the Service depends on an interface, not a concrete class.
🛠 Preparing the Test Project
A separate test project is created alongside the main application.
- Test framework: xUnit
- Mocking library: Moq
- Reference: the main
HelloFlowproject
Moq allows us to create fake implementations of interfaces with minimal effort.
🧠 What Are We Actually Testing?
This test verifies a very specific behavior:
When HelloService.GetHello() is called,
does it request the repository to save the data exactly once?
We are not testing storage. We are not testing LINQ. We are testing behavior.
🧪 Unit Test Code
using HelloFlow.Models;
using HelloFlow.Repositories;
using HelloFlow.Services;
using Moq;
using Xunit;
namespace HelloFlow.Tests;
public class HelloServiceTests
{
[Fact]
public void GetHello_Should_Call_Save_In_Repository()
{
// Arrange
var mockRepo = new Mock<IHelloRepository>();
var service = new HelloService(mockRepo.Object);
// Act
var result = service.GetHello("TestUser");
// Assert
mockRepo.Verify(
x => x.Save(It.IsAny<HelloResponse>()),
Times.Once
);
Assert.Equal("Hello, TestUser!", result.Message);
}
}
`
🔍 Test Structure: Arrange → Act → Assert
All professional unit tests follow the same structure:
- Arrange: prepare the environment and dependencies
- Act: execute the method under test
- Assert: verify the expected behavior
This structure scales from small services to large systems.
🧠 Why This Test Matters
This single test proves several important things:
- Isolation: The Service is tested without executing any repository code.
- Speed: No database, no I/O, no setup cost.
- Safety: Repository implementations can change without breaking service tests.
This is the foundation of automated testing in CI/CD pipelines and a key concept in DevOps practices.
🧠 One-Sentence Summary
This step proves that the Service behaves correctly — without repositories, databases, or infrastructure.
✍️ My Notes & Reflections
- As the system grows step by step, it is becoming more challenging, but at the same time it feels more organized.
- I am starting to sense how a system behaves like a factory, where each component plays a specific role and works together.
Top comments (1)
This is a really solid way to frame it — especially for people early in C# who keep hearing “use interfaces” without ever seeing why.
You nailed the core point: interfaces aren’t about abstraction trophies, they’re about control. The moment you can swap a real dependency for a mock and prove behavior in isolation, the whole thing clicks. No DB, no side effects, no guessing — just “did this service do what it promised to do?”
I also like that you focused the test on interaction, not implementation. Verifying Save was called exactly once is a very grown-up test choice. It teaches that unit tests are about behavioral contracts, not re-running the same logic twice.
The factory analogy at the end is spot on too. Once responsibilities are clear and boundaries are enforced, systems stop feeling fragile and start feeling… predictable. That’s when testing stops being scary and starts being boring (in the best way).
Great progress for day 08 — this is one of those concepts that quietly levels people up without them realizing it.