Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How is it possible to write unit test before write source code?

As the unit test is a white box test, it assumes that you must know in advance all the cases your code must treat, all the client objects (aka Mock object in the test) your code must deal with, and the correct order in which client objects must appear in the code (since unit test take in account the calling of mock object). In other words you must know exactly the detailed algorithm of your code. Before you can exactly know the algorithm of your code you must write it first!

From my point of view I don't see how it's possible to write correct unit test before writing source code. Nevertheless it's possible to write functional tests first since functional tests are sort of user requirement. Your advice? Best regard

AN EXAMPLE IS GIVEN FOR THIS QUESTION:
How to write test code before write source code when they are objects dependencies?

like image 872
Belin Avatar asked Dec 02 '25 17:12

Belin


2 Answers

In other word you must know exactly the detailled algorithm of your code.

Not quite. You must know exactly the detailed behavior of your code, as observed from outside the code itself. The algorithm which achieves this behavior, or combination of algorithms, or any levels of abstraction/nesting/calculations/etc. are immaterial to the tests. The tests only care that the desired result is achieved.

The value of the tests, then, is that they are the specifications of how the code should behave. So the code is free to change all you want, as long as it can still be validated against the tests. You can improve performance, refactor for readability and supportability, etc. The tests ensure that the behavior remains unchanged.

For example, suppose I want to write a function which adds two numbers. You may know in your head how you're going to implement it, but put that knowledge aside for a moment. You're not implementing it yet. First, you're implementing the test...

public void CanAddIntegers()
{
    var addend = 1;
    var augend = 1;
    var result = MyMathObject.Add(addend, augend);
    Assert.AreEqual(2, result);
}

Now that you have a test, you can implement the method...

public int Add(int addend, int augend)
{
    return ((addend * 2) + (augend * 2)) / 2;
}

Whoa. Wait a minute... Why on Earth did I implement it like that? Well, from the perspective of the test, who cares? It passes. The implementation meets the requirements. And now that I have a test, I can safely refactor the code...

public int Add(int addend, int augend)
{
    return addend + augend;
}

That's a little more sane. And the test still passes. In fact, I can further reduce the code...

public int Add(int addend, int augend)
{
    return 2;
}

Guess what? The test still passes. It's the only test we have, it's the only specification given, so the code "works." So clearly we need to improve the tests to cover more cases. Writing more tests will give us the specifications we need to write more code.

In fact, that last implementation should have been the first one, according to the third rule of TDD:

You are not allowed to write any more production code than is sufficient to pass the one failing unit test.

So in a purely-Uncle-Bob-driven TDD world, we would have written that last implementation first, then written more tests and gradually improved the code.

This is known as the Red, Green, Refactor cycle. It's very well illustrated in a simple slightly-less-contrived example, the Bowling Game. The purpose of that exercise is to practice that cycle:

  1. First, write a test which expects certain behavior. This is the Red part of the cycle, because the test will fail without the behavior in place.
  2. Next, write code to exhibit that behavior. This is the Green part of the cycle, because its purpose is to get the test to pass. And only to get the test to pass.
  3. Finally, refactor the code and improve it. This is, naturally, the Refactor part of the cycle.

Where you're getting stuck is that you're perpetually in the Refactor part of the cycle. You're already thinking about how to make the code better. What algorithm would be correct, how to optimize it, how it ultimately should be written. To that end, TDD is an exercise in patience. Don't write the best code... yet.

  1. First, determine what the code should do, and nothing more.
  2. Next, write code that does it, and nothing more.
  3. Finally, improve that code and make it better.

UPDATE

I came across something which reminded me of this question, and a random though occurred to me. Perhaps I misinterpreted the circumstances of what you're asking. How are you managing your dependencies? That is, what sort of dependency injection methodology are you using? It sounds like that may be the root of the issue being discussed here.

For about as long as I can remember, I've used something like Common Service Locator (or, more commonly, home-grown implementations of the same concept). And in doing so I tend toward a very specific style of dependency injection. It sounds like you're using a different style. Constructor injection, perhaps? I'll assume constructor injection for the sake of this answer.

So then let's say, as you indicate, that MyMathObject has dependencies on MyOtherClass1 and MyOtherClass2. Using constructor injection, that makes the footprint of MyMathObject look like this:

public class MyMathObject
{
    public MyMathObject(MyOtherClass1 firstDependency, MyOtherClass2 secondDependency)
    {
        // implementation details
    }

    public int Add(int addend, int augend)
    {
        // implementation details
    }
}

And so, as you indicate, the tests need to supply the dependencies or mocks thereof. In the footprint of the class there's no indication of the actual use of MyOtherClass1 or MyOtherClass2, but there is indication of the need for them. As dependencies they are loudly advertised by the constructor.

So this begs the question you've asked... How can one write the tests first when one hasn't implemented the object yet? Again, there's no indication of the actual use in only the external-facing design of the object. So the dependency is an implementation detail that needs to be known.

Otherwise, you'd first write this:

public class MyMathObject
{
    public int Add(int addend, int augend)
    {
        // implementation details
    }
}

Then you'd write your tests for it, then you'd implement it and discover the dependencies, then you'd re-write your tests for it. Therein lies the problem.

The problem that you've found, however, isn't an issue of the tests or of Test Driven Development. The problem is actually in the design of the object. Despite the fact that // implementation details have been glazed over, there's still an implementation detail that's escaping. There's a leaky abstraction:

public class MyMathObject
{
    public MyMathObject(MyOtherClass1 firstDependency, MyOtherClass2 secondDependency)
    {                   ^---Right here                 ^---And here

        // implementation details
    }

    public int Add(int addend, int augend)
    {
        // implementation details
    }
}

The object isn't encapsulating and abstracting its implementation details sufficiently. It's trying to, and the use of dependency injection is a major step toward that. But it isn't fully there yet. This is because the dependencies, which are implementation details, are externally visible and externally known by other objects. (In this case, the testing object.) So in order to satisfy the dependencies and make MyMathObject work, external objects need to know about its implementation details. They all do. The test object, any production code objects which use it, anything and everything that relies on it in any way.

To that end, you might want to consider switching around how the dependencies are managed. Instead of something like constructor injection or setter injection, further invert the management of dependencies and have the object internally resolve them through yet another object.

Using the aforementioned service locator as a starting pattern, it's pretty easy to craft an object whose sole purpose (whose single responsibility) is to resolve dependencies. If you're using a dependency injection framework then this object is generally just a pass-through for the framework's functionality (but abstracting the framework itself... so one less dependency, which is a good thing). If using home-grown functionality than this object abstracts that functionality.

But what you end up with is something like this within MyMathObject:

private SomeInternalFunction()
{
    var firstDependency = ServiceLocatorObject.Resolve<MyOtherClass1>();
    // implementation details
}

So now the footprint of MyMathObject, even with dependency injection, is:

public class MyMathObject
{
    public int Add(int addend, int augend)
    {
        // implementation details
    }
}

No leaky abstractions, no externally-known dependencies. Tests don't need to be changed as implementation details are changed. This is another step toward de-coupling the tests from the objects they're testing.

like image 198
David Avatar answered Dec 06 '25 14:12

David


Clearly you cannot write a test if you have no understanding of what the "software" intent is, but it is absolutely possible if the requirements or specifications are detailed.

You can write something that fits the requirement that will fail; then produce the minimum amount of work to make the test pass based on the specification.

Unless you're some kind of genius - this first cut will need refactoring and abstractions introduced, patterns, maintainability, performance and all kinds of other things accounted for.

So if the requirements are understood, you can test first - but the test will not pass until the implementation comes together and only the implementation to make the test pass is required.

It won't always fit the bill in reality to work in this way - particularly if specification is hard to come by. You need to be careful not to blindly storm down this path if you're not getting what you need as a developer. It's also, often, impossible to achieve when inheriting code or adding to "brownfield" projects. As a developer, it's important to work out the practicalities early on.

like image 34
SpaceBison Avatar answered Dec 06 '25 16:12

SpaceBison



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!