TDD: why might it be wrong to let app code know it is being tested, not run?

后端 未结 5 500
说谎
说谎 2021-01-19 19:35

In this thread, Brian (the only answerer) says \"Your code should be written in such a fashion that it is testing-agnostic\"

The single comment says \"Your code shou

5条回答
  •  暗喜
    暗喜 (楼主)
    2021-01-19 19:41

    I will split this answer into two sections. First I'll share my thoughts on Brian's answer, then I'll share some tips on how to test effectively.

    An explanation of Brian's answer

    There appear to be two key ideas that Brian is hinting at. I will address each one individually.

    Idea 1: Production code should not depend on tests

    Your code should be written in such a fashion that it is testing-agnostic.

    The production code should not depend on tests. It should be the reverse.

    There are multiple reasons for this:

    1. Changing your tests will not change the behaviour of your code.
    2. Your production code can be compiled and deployed independently of the test code.
    3. Your code won't need to be recompiled when updating the tests.
    4. Your production code cannot possibly fail due to unintended side effects from not running the test code.

    Note: Any decent compiler will remove the test code. Although I don't think this is an excuse to poorly design/test your system.

    Idea 2: You should test abstractions rather than implementations

    Whatever environment you test in should be as close to real-world as possible.

    It sounds like Brian might be hinting at this idea within his answer. Unlike the last idea, this one isn't universally agreed upon, so take it with a grain of salt.

    By testing abstractions, you develop a level of respect for the unit being tested. You agree that you will not hoke around with its internals and spy on its internal state.

    Why shouldn't I spy on the state of objects during testing?

    By spying on the innards of an object, you are causing these problems:

    1. Your tests will tie you to a specific implementation of a unit.

      For example...
      Want to change your class to use a different sorting algorithm? Too bad, your tests will fail because you've asserted that the quicksort function must be called.

    2. You will break encapsulation.

      By testing the internal state of an object, you will be tempted to loosen some of the privacy that the object has. This will mean that more of your production code will also have increased visibility into your object.

      By loosening the encapsulation of your object, you are tempting other production code to also depend on it. This can not only tie your tests to a specific implementation, but also your entire system itself. You do not want this to happen.

    Then how do I know if the class works?

    Test the pre-conditions and post-conditions/results of the method being called. If you need more complex tests, look at the final section I've written on mocking and dependency injection.

    Mini note

    I don't think it's necessarily bad to have an if (TEST_MODE) in your main method as long as your production code remains independent of your tests.

    For example:

    public class Startup {
    
        private static final boolean TEST_MODE = false;
    
        public static void main(String[] args) {
            if (TEST_MODE) {
                TestSuite testSuite = new TestSuite();
                testSuite.execute();
            } else {
                Main main = new Main();
                main.execute();
            }
        }
    }
    

    However, it becomes a problem if your other classes know that they're running in test mode. If you have if (TEST_MODE) throughout all of your production code, you're opening yourself up to the problems I've mentioned above.

    Obviously in Java you would use something like JUnit or TestNG instead of this, but I just wanted to share my thoughts on the if (TEST_MODE) idea.

    How to test effectively

    This is a very large topic, so I'll keep this section of the answer short.

    • Instead of spying on internal state, use mocking and dependency injection.

      With mocks, you can assert that a method of a mock you've injected has been called. Better yet, the dependency injection will invert your classes' dependency on the implementation of whatever you've injected. This means you can swap out different implementations of things without needing to worry.

      This completely removes the need to hoke around inside your classes.


    If there was one book I'd strongly recommend reading, it would be Modern C++ Programming with Test-Driven Development by Jeff Langr. It's probably the best TDD resource I've ever used.

    Despite having C++ in the title, its main focus is definitely TDD. The introduction of the book talks about how these examples should apply across all (similar) languages. Uncle Bob even states this in the foreword:

    Do you need to be a C++ programmer to understand it? Of course you don't. The C++ code is so clean and is written so well and the concepts are so clear that any Java, C#, C, or even Ruby programmer will have no trouble at all.

提交回复
热议问题