=== Test Driven Development === * Have you guys ever heard of Test Driven Development (TDD)? - That's a software development approach in which tests are written before production code. * What are the steps of TDD? 1. Write a test. 2. Run all the existing tests. 3. Write code to pass tests that fail. 4. Run all the tests again. 5. If there are tests failing, go back to 3. 6. Refactor the code. 7. If there is still code to do, go back to step 1. * Can you think about advantages of writing tests first? - Clear goal: pass the test - Tests work as documentation - Help maintaining the code * Can you think about disadvantages? - Software might contain too many tests. - It is hard to try ideas. - Development cycle might become more expensive. === Example: Sorted List === * Write a class that implements a sorted list. * Which methods this class should have? - get_min() - get_max() - remove_min() - remove_max() - size() - insert() * Can you write tests for these methods? * Can you test size()? #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #include "doctest.h" #include "SortedList.h" TEST_CASE("Testing the size() method") { SortedList sl; CHECK(sl.size() == 0); } * Can you test insert()? TEST_CASE("Testing the insert() method") { SortedList sl; CHECK(sl.size() == 0); sl.insert(2); CHECK(sl.size() == 1); sl.insert(3); CHECK(sl.size() == 2); } * Can you test get_min()? TEST_CASE("Testing the get_min() method") { SortedList sl; CHECK(sl.size() == 0); sl.insert(2); CHECK(sl.get_min() == 2); sl.insert(1); CHECK(sl.get_min() == 1); } * Can you test get_max()? TEST_CASE("Testing the get_max() method") { SortedList sl; CHECK(sl.size() == 0); sl.insert(2); CHECK(sl.get_max() == 2); sl.insert(3); CHECK(sl.get_max() == 3); } * Can you test remove_min()? TEST_CASE("Testing the remove_min() method") { SortedList sl; CHECK(sl.size() == 0); sl.insert(2); CHECK(sl.get_min() == 2); sl.insert(1); CHECK(sl.get_min() == 1); sl.remove_min(); CHECK(sl.get_min() == 2); CHECK(sl.size() == 1); } * Can you test remove_max()? TEST_CASE("Testing the remove_max() method") { SortedList sl; CHECK(sl.size() == 0); sl.insert(2); CHECK(sl.get_max() == 2); sl.insert(3); CHECK(sl.get_max() == 3); sl.remove_max(); CHECK(sl.get_max() == 2); CHECK(sl.size() == 1); } * Why we say that tests work as documentation? * Will this program compile? Test0.cpp:3:10: fatal error: 'List.h' file not found #include "List.h" ^~~~~~~~ 1 error generated. * So, this is step (1) and (2) of TDD. Can you try to move on to step (3)? First, you need to write a minimum program that compiles. #ifndef SORTED_LIST_H #define SORTED_LIST_H template class SortedList { public: unsigned size() const { return 0; } void insert(T e) { } T get_max() const { return 0; } T get_min() const { return 0; } void remove_min() { } void remove_max() { } }; #endif * Let's move on to step (4). How many tests can your program pass? [doctest] test cases: 6 | 1 passed | 5 failed | 0 skipped [doctest] assertions: 20 | 6 passed | 14 failed | [doctest] Status: FAILURE! * Which test passes? $> ./a.out -s * Can you try to pass the tests for the insert method? #ifndef SORTED_LIST_H #define SORTED_LIST_H template class SortedList { unsigned _size; public: unsigned size() const { return _size; } void insert(T e) { _size++; } T get_max() const { return 0; } T get_min() const { return 0; } void remove_min() { } void remove_max() { } }; #endif * This solution is not good, right? What are its problems? * The problems will surface once we move on to the other methods. Let's try passing the get_min test. Can you do it? * How should we try to implement this list? * What about the code below, would it work? #ifndef SORTED_LIST_H #define SORTED_LIST_H template class SortedList { unsigned _size; T _e; public: unsigned size() const { return _size; } void insert(T e) { _e = e; _size++; } T get_max() const { return 0; } T get_min() const { return _e; } void remove_min() { } void remove_max() { } }; #endif * What is the problem of the program above? Can you extend it to pass the get_max() test? [doctest] test cases: 6 | 4 passed | 2 failed | 0 skipped [doctest] assertions: 20 | 14 passed | 6 failed | [doctest] Status: FAILURE! * We are not doing bad! What about remove_min()? Would it work? * Ok, we are postponing some serious work. How can you implement something that has a chance of passing all the tests? #ifndef SORTED_LIST_H #define SORTED_LIST_H #include template class SortedList { std::set _set; public: unsigned size() const { return _set.size(); } void insert(T e) { _set.insert(e); } T get_max() const { return 0; } T get_min() const { auto r = _set.begin(); return *r; } void remove_min() { } void remove_max() { } }; #endif * We are passing the same tests as before, but we still need to pass get_max(). Can you do it? #ifndef SORTED_LIST_H #define SORTED_LIST_H #include template class SortedList { std::set _set; public: unsigned size() const { return _set.size(); } void insert(T e) { _set.insert(e); } T get_max() const { auto r = _set.rbegin(); return *r; } T get_min() const { auto r = _set.begin(); return *r; } void remove_min() { } void remove_max() { } }; #endif * What is this rbegin() method? * Ok. Can you now try to pass the remove_min() test? #ifndef SORTED_LIST_H #define SORTED_LIST_H #include template class SortedList { std::set _set; public: unsigned size() const { return _set.size(); } void insert(T e) { _set.insert(e); } T get_max() const { auto r = _set.rbegin(); return *r; } T get_min() const { auto r = _set.begin(); return *r; } void remove_min() { _set.erase(get_min()); } void remove_max() { } }; #endif * Passing remove_max() should also be easy, right? void remove_max() { _set.erase(get_max()); } * This implementation looks suspicious. Could you try to add more tests to see if it is correct? Or, in other words, go back to step (1). Which tests could you consider? * Can you write a test to check if the elements are removed sorted from the list, if we use remove_min() until the list becomes empty? #include TEST_CASE("Testing if numbers are removed in an ordered way:") { SortedList sl; for (int i = 0; i < 10; i++) { int e = rand() % 100; sl.insert(e); } int min = INT_MIN; while (sl.size() > 0) { CHECK(min <= sl.get_min()); min = sl.get_min(); sl.remove_min(); } } * What is INT_MIN above? From where does it come? * Can you do the same with remove_max, to sort in the reverse order? TEST_CASE("Testing if remove_max sorts in reverse order:") { SortedList sl; for (int i = 0; i < 10; i++) { int e = rand() % 100; sl.insert(e); } int max = INT_MAX; while (sl.size() > 0) { CHECK(max >= sl.get_max()); max = sl.get_max(); sl.remove_max(); } } * Let's try to think about the contract of SortedList. What are the pre-conditions? * Can you design a test to check the preconditions of get_min()? TEST_CASE("Reading minimum from empty list.") { SortedList sl; CHECK(sl.size() == 0); CHECK_THROWS_AS(sl.get_min(), const char*); } * What is the outcome of this test? Test2.cpp:93: ERROR: CHECK_THROWS_AS( sl.get_min(), const char* ) did NOT throw at all! * Can you improve the code of get_min(), so that it passes the test? template class SortedList { // ... public: // ... T get_min() const { if (size()) { auto r = _set.begin(); return *r; } else { throw "Error: reading minimum of empty sorted list."; } } // ... }; * Can you design tests to check the pre-conditions of get_max(), remove_min() and remove_max()? TEST_CASE("Reading maximum from empty list.") { SortedList sl; CHECK(sl.size() == 0); CHECK_THROWS_AS(sl.get_max(), const char*); } TEST_CASE("Removing minimum from empty list.") { SortedList sl; CHECK(sl.size() == 0); CHECK_THROWS_AS(sl.remove_min(), const char*); } TEST_CASE("Removing maximum from empty list.") { SortedList sl; CHECK(sl.size() == 0); CHECK_THROWS_AS(sl.remove_max(), const char*); } * Can you implement a version of get_max() that passes this test? template class SortedList { // ... public: // ... T get_max() const { if (size()) { auto r = _set.rbegin(); return *r; } else { throw "Error: reading maximum of empty sorted list."; } } // ... }; * That's odd: why did this version already ensured that we passed the test of remove_max()? === Back to the Debate === * So, do you think TDD is good? * What do you think is better: to write the tests before or after you code the program? Or not to write tests at all? * What are disadvantages of TDD? - High upfront cost. You need to design the tests before producing the code, and designing the tests takes time. - Hard to adapt to changing requirements. What if you have to change the interface of classes? You will need to change lots of tests. - Tests need to be maintained too. So, that's more code to be worried about. - TDD makes it hard to prototype ideas. When we prototype, we just want to try something to see if it works, but then we need to write the test before... * What are the advantages? - Thinking ahead: you need to plan your interfaces before you start coding them. - Constant refactoring. In general, the code end up clearer and cleaner. - Clear goals: our mind works better if we know what needs to be achieved. - Documentation: the test documents the code.