lip service to but don’t actually do. • Test-driven development (TDD) is a methodology for writing software where tests are written before implementation code. • In the traditional style of development, you write code, then write tests to verify that it works as expected. • With TDD, you write tests for code that does not exist, then write just enough code to make the tests pass.
code necessary to satisfy requirements. • Helps ensure good test coverage. • Helps find bugs as they’re introduced. • Provides feedback on code complexity. • Prevents you from writing code that is difficult to test. • Helps you write code that is well factored, correct, and resistant to change. • Overall: drives the design of your software.
is likely to be more verbose. • Code is much more likely to be untested, which is extremely dangerous. • Bugs are more likely. • Objects and methods are much more likely to have high cyclomatic complexity. • It’s usually much more difficult to backfill tests. • Code is harder to change with confidence. • Overall: creates a system that is more difficult to change and more likely to encounter bugs and regressions.
greet(name): return "Hello, %s!" % name ! # Write a test to verify correctness. This test will pass. def test_greet(): assert greet("StarCraft") == "Hello, StarCraft!" ! # But what if the function was written like this? # The test would still pass, even though the behavior is # not the same! def greet(name): return "Hello, StarCraft!"
test. def test_greet(): assert greet("StarCraft") == "Hello, StarCraft!" ! # Run the test and it fails, because the greet method # doesn't exist. Let's define it. def greet(): pass ! # Run the test and it fails, because greet expects # one argument. Let's add an argument. def greet(name): pass ! # Run the test and it fails, because greet does not # return the expected string. Let's make it return # exactly what is expected. This is the simplest # possible code to make the test pass. (Sometimes # called a “slime.”) def greet(name): return "Hello, StarCraft!"
case # The original test. def test_greet(): assert greet("StarCraft") == "Hello, StarCraft!" ! # A test checking for a different name. def test_greet_different_name(): assert greet("Charles") == "Hello, Charles!" ! # The original test continues to pass, but the new test # fails, because StarCraft is greeted, not Charles. This # forces us to generalize our production code. def greet(name): return "Hello, %s!" % name ! # Run the tests and they pass. If someone changes the # production code back to the static string, they'll know # they broke something right away because the second # test will fail!
case without TDD. • However: TDD is thorough and doesn’t make assumptions. • TDD makes it more likely to notice issues you would not have otherwise. • TDD helps you code in small, iterative steps, which improve your own understanding of the system. It’s easier to see the forest for the trees, and vice versa.
on top of the ideas of TDD and XP/Agile. • BDD focuses on the behavior of the units under test, rather than the details of their implementation. • BDD helps align software with the requirements supplied by stakeholders by focusing on business value and using a common language. • BDD emphasizes testing the system as the consumer will use it.
its business requirements, BDD uses an approach to testing known as “outside-in.” • Testing begins at the highest level. When a high level test fails, you dive in deeper and begin testing the individual component that caused the failure.
tests intended to verify the behavior of the entire system as a whole. • Acceptance tests: High level tests that make assertions about specific business requirements. e.g. “As a user, I should be able to view my profile.” • Integration tests: Tests that verify the boundaries between units. Where unit tests should use test doubles to stub their dependencies, integration tests verify that those assumptions are correct. • Unit tests: Very granular tests that verify the behavior of the smallest pieces of the system, isolated from their dependencies. In object oriented software, units are usually objects.
to force a generalized implementation of a function, outside-in allows tests to force the creation of objects solely based on need. • In effect, testing drives the design of the system. • Example: A user signing up: The acceptance test forces you to create a user sign up page, which forces a view, which needs to create a user record, which forces a model.
TDD mantra used to describe the basic workflow. • Red: Write a test, run it, watch it fail, and note why it failed. • Green: Add just enough code to correct the specific failure. Run the test again and address any new failures. Continue doing this until the test passes. • Refactor: Now that the test is passing, do any internal clean up necessary, e.g. extracting duplication into additional methods. Keep running the tests after each change to ensure your changes haven’t caused a regression.
statement can have a very large impact on the way you write code. • When you write a test for code that doesn’t yet exist, you have the opportunity to design the ideal interface for your system. • Write tests for code imagining the ideal API. TDD will ensure that your ideal is the actual result.