Using an example
The goal of the "fake object" is to inherit the interface and break dependencies so that way you can unit test it. [..] I don't understand how these are examples of legitimate tests.
Think of it this way: your car won't start. A car has many parts, and if one of them fails, the car won't start. But which part is failing at the moment?
One way of finding out which part is failing, is by taking the car apart and testing each part individually. This is what a unit test is. So let's continue the example and let's test if the piston is faulty.
A piston is a device which takes in air and fuel, and outputs kinetic energy (= movement). The fuel is retrieved from the fuel tank, the air is retrieved from the air intake. So an analogous code example would be:
public class Piston
{
private readonly AirSource airSource;
private readonly FuelSource fuelSource;
public Piston(AirSource airSource, FuelSource fuelSource)
{
this.airSource = airSource;
this.FuelSource = fuelSource;
}
public Movement Fire()
{
var fuel = fuelSource.GetFuel();
var air = airSource.GetAir();
// This is just some silly example code
if (fuel != null && air != null)
return new Movement();
else
throw new EngineStallException();
}
}
Don't get too bogged down with the actual content of the method - it's silly code that I kept simple because the details don't matter. The main goal is to understand that the fuel and air sources are dependencies, and the piston's behavior is influenced by the behavior of its connected fuel and air source.
In the real car (= production code), the piston is connected to the car's fuel tank and air intake, along the lines of:
var piston = new Piston(myCar.AirIntake, myCar.FuelTank);
However, and this is key to understanding unit testing: when we are unit testing the piston, we don't want to use the real dependencies, because any issues in the dependencies (e.g. a faulty air intake) will lead us to wrongly conclude that there's something wrong with the piston (because it depends on the air intake), even though there's nothing wrong with the piston itself and it would be working if its air intake was working properly.
Unit testing is done by swapping out the dependencies for ones where you can guarantee that they are working correctly. If you know for a fact that the dependencies are not faulty, then any faulty behavior from the piston proves that there is a problem in the piston itself.
So when we test the piston, we do something like this:
var piston = new Piston(new InfiniteAirSource(), new InfiniteFuelSource());
var movement = piston.Fire();
Assert.IsNotNull(movement);
This is our simple unit test: when the piston is provided with air and fuel, is it able to generate movement?
- If the test passes, then the piston is working as intended.
- If the test fails, then there is an issue with the piston.
The test result immediately answers the question "does the piston work?". Compare this to when we hadn't mocked our dependencies:
var piston = new Piston(myCar.AirIntake, myCar.FuelTank);
var movement = piston.Fire();
Assert.IsNotNull(movement);
- If the test passes, then the piston and its dependencies are working as intended.
- If the test fails, ...
- maybe the piston failed?
- maybe the air intake failed?
- maybe the fuel tank failed?
This test no longer answers the question "does the piston work?", because when the test fails, we don't actually know if the piston is to blame or not.
Remember, our car won't start, and we still haven't figured out why it won't start. But we know that it won't start, which means that something isn't working. If we perform unit tests on all of the car's parts, then (at least) one of these unit tests will fail. This will reveal to us which part we need to fix in order to get the car working again.
That is the purpose of unit tests: figuring out which specific component is faulty.
Small note: unit vs integration tests
Note that there is still some value to be had from testing multiple parts together, but that's called an integration test, not a unit test.
My opinion is that integration tests are only meaningful when all involved components are individually unit tested before the combined integration test is run, but that's a discussion for a different day.
Your test case
In the examples below, the author hard-codes and sets myFakeManager.WillBeValid
within the tests to true, passes myFakeManager
to the LogAnalyzer
and immediately asserts that what they just set myFakeManager.WillBeValid
to is true... What they literally just set it to?!
You're not thinking of this the right way. You know what the content of the IsValidLogFileName
method is, and you're thinking that everybody knows that. They don't.
You are able to tell me if your car starts or not, because you understand its public behavior (turn the key => start the car) and can tell that something is wrong (I turn the key but the car won't start).
However, you are not a mechanic and don't know how a car is built, or what could be causing it to not start. But you don't need to know how the car works to test if the car works.
Unit tests only care about external behavior, they don't care about implementation.
and immediately asserts that what they just set myFakeManager.WillBeValid
to is true...
The test does not assert that myFakeManager.WillBeValid
returns true
.
The test asserts that a LogAnalyzer
behaves a certain way ("the car starts") when its dependencies are in a particular state ("the key is turned").
In this particular case, the behavior is purely defined by the value of IExtensionManager.WillBeValid
, but that is not always going to be the case for everything you test.
The value of testing
How are these tests in LogAnalyzerTests.cs useful?
Even if the method merely passes a value from its dependency to its consumer, the test still has value, as the test effectively confirms that a LogAnalyzer
correctly listens to its dependencies and behaves as expected.
A student attends a class, learns something, and then takes an exam on the subject matter they learned about.
Based on the question you posted here, your interpretation would be that this exam is useless since it tries to assert that the student knows the subject matter that they've learned - and it's pointless because (allegedly) having attended a class on the subject inherently means that you know the subject matter. That's clearly not the right idea here.
You're missing the point of the test. The test is there to confirm that the information passed from one source (the teacher / the extension manager) to another (the student / the consumer of the LogAnalyzer
object).
When the test passes, that proves to you that the student correctly learned the lesson that the teacher provided. In your test case, it proves to you that the LogAnalyzer
actually uses its dependencies the way it is expected to use them.