views:

213

answers:

3

Hi!

Assume we have a class UserService with attribute current_user. Suppose it is used in AppService class.

We have AppService covered with tests. In test setup we stub out current_user with some mock value:

UserService.current_user = 'TestUser'

Assume we decide to rename current_user to active_user. We rename it in UserService but forget to make change to its usage in AppService.

We run tests and they pass! Test setup adds attribute current_user which is still (wrongly but successfully) used in AppService.

Now our tests are useless. They pass but application will fail in production.

We can't rely on our test suite ==> TDD is not possible.

Is TDD broken in Python?

A: 

Before the change, something should be different about the object's behavior, depending on the value of current_user. Let's call that something predicate(). And pardon my python; consider this pseudocode:

UserService.current_user = 'X'
assertFalse(obj.predicate())
UserService.current_user = 'Y'
assertTrue(obj.predicate())

OK? So, that's your test. Get it to pass. Now change the class under test so that current_user is renamed to active_user. Now the test will fail, either at the first assertion, or at the second. Because you're no longer changing the value of the field formerly known as current_user, so predicate will be false or true in both cases. Now you have a very focused test that will alert you when the class changes in such a way as to invalidate the other test's setup.

Carl Manaster
Maybe I don't understand how `predicate` should look like. If `predicate` is using `current_user`, this test won't fail after I rename attribute of `UserService` to `active_user` because this test sets up `current_user` by itself.
Konstantin Spirin
Your existing test relies on that setup - it's not sensitive to the active_user / current_user split. So you need a test that is, if you want such changes to be automatically detected. As a *unit* test, you want to be testing small things - you don't want your existing test to both test whatever it's testing, *and* the name of the field. So add a new test that is sensitive only to the field name.
Carl Manaster
Ideal test to check field name will be `assert(hasattr(UserService, 'current_user'))`. This test will fail after I rename to `active_user`, I'll fix it. But I will forget about the second test that will be green all the time.
Konstantin Spirin
+2  A: 

The problem really is not in TDD, nor Python. First of all, TDD does not give you a proof that when all your tests pass, your application is good. Imagine e.g. multiplyBy2() function, which can be tested with input 1,2,3 and output 2,4,8 and now imagine, you implemented multiplyBy2 as squaring. All your tests pass, you have 100% code coverage and your implementation is wrong. You have to understand, that TDD can only give you assurance, that once your test fails, something is wrong with your app, nothing more, nothing less. So as suggested in other answer, the problem is in that you do not have test that fails. If you had used some statically typed language, compiler would do that test for you and complain about using non-existent method. This does not mean you should use statically typed language, it just means you need to write more tests in dynamically typed language. If you are interested in enforcing correctness of the code, you should look at design by contract for ensuring correctness at least during runtime and formal specifications to have proofs for at least some algorithms, but this is I guess quite far from standard coding.

Gabriel Ščerbák
TDD also gives me assurance that my old functionality is still there (aka there's no regression). `DBC` sounds like a possible solution
Konstantin Spirin
@Konstantin no it does not, you have not understood my point. Take my multiplication example, assume it is implemented the right way, your tests pass, you make a change - substitute multiplication with squaring and your tests still pass, but you might had introduced regression bug. Really only failing tests give you some real information. I like TDD very much, but I think it is important to really understand how it works and what it does.
Gabriel Ščerbák
OK, my question is how to write test that tests my production code, not test that tests itself. I can't agree that if test is not failing it gives you no information. Every test incorporates a piece of knowledge about the system, invariant if you wish. Invariant shouldn't change and if after refactoring test passes that means that functionality you had before you still have now. If `test_multiplyBy2` is not failing even when you are using square then the test is bad. I'm looking for a way to write a good test.
Konstantin Spirin
@Gabriel - 3^2 != 8; 1^2 != 2. Your point is certainly correct, but your example could use some repair. 0^2 == 0 * 2; 2^2 == 2 * 2.
Carl Manaster
@Konstantin excuse me, I did not wanted to sound like it says nothing, it just gives you no proof. It gives you information, but just for that case, that is the thing I want to hint at. The thing with the invariant is that it is as good as restrictive it is, e.g. test which always return true is a test, but means nothing. As you state in the end - the issue is writing good tests.
Gabriel Ščerbák
@Carl Manaster those test cases you selected which fail really proove how much I suck at math:)... In my defense I learnt at university that by <a href="http://en.wikipedia.org/wiki/Interpolation">interpolation</a> you can generate infinite number of functions for given finite set of function points (+some other preconditions...) meaning, that there are functions which to be tested for each case would require infinite number of tests.
Gabriel Ščerbák
A: 

OK, I have found the solution. Python library Mock does what I want.

Below is the code I end up with.

Model and service definitions:

class User(object):
    def __init__(self):
        self.roles = []


class UserService(object):
    def get_current_user(self):
        return None # get from environment, database, etc.

    current_user = property(get_current_user)


class AppService(object):
    def __init__(self, userService):
        self.userService = userService

    def can_write(self):
        return 'admin' in self.userService.current_user.roles

Here's how to test can_write method of AppService with different users:

class AppServiceTests(unittest.TestCase):
    def test_can_write(self):
        user = User()

        @patch_object(UserService, 'current_user', user)
        def can_write():
            appService = AppService(UserService())
            return appService.can_write()

        user.roles = ['admin']
        self.assertTrue(can_write())

        user.roles = ['user']
        self.assertFalse(can_write())

If you rename property current_user only in class UserService, you'll get error when trying to patch the object. This is the behavior I was looking for.

This time TDD in Python is not broken. Python is better than I thought. Woo-hoo!

Konstantin Spirin