constructor classmethods

teppo.pera at gmail.com teppo.pera at gmail.com
Wed Nov 9 03:52:06 EST 2016


keskiviikko 9. marraskuuta 2016 2.25.59 UTC Steve D'Aprano kirjoitti:
> On Wed, 9 Nov 2016 10:01 am, teppo... at gmail.com wrote:
> 
> > Generally, with testing, it would be optimal to test outputs of the system
> > for given inputs without caring how things are implemented.
> 
> I disagree with that statement.

I'm aware of differences of white-box and black-box testing. White-box is based on knowledge of code and black-box is based on knowledge of requirements. Knowledge of code doesn't mean that test code queries the internals of the code being tested. More specifically, example._queue should not ever be in test code (this is what I mean not caring about the implementation). With white-box testing, you can locate specific branch, say exception, and test that, if it affects the output of the function somehow. If not, what it is doing there?

> But that churn only applies to one person: the maintainer of the test. It 
> doesn't affect users of the code. It is certainly a cost, but it is 
> narrowly focused on the maintainer, not the users. 

What happens when the the maintainer gets sick or leaves the company? Maintainer of the tests should be team of developers. Focusing maintenance of tests to single person will lead to project disaster sooner or later and this correlates well with size of the codebase. 


> > That way, any 
> > changes in implementation won't affect test results. Very trivial example
> > would be something like this:
> > 
> > def do_something_important(input1, input2, input3)
> >     return  # something done with input1, input2, input3
> > 
> > Implementation of do_something_important can be one liner, or it can
> > contain multiple classes, yet the result of the function is what matters.
> 
> Certainly. And if you test do_something_important against EVERY possible
> combination of inputs, then you don't need to care about the
> implementation, since you've tested every single possible case.
> 
> But how often do you do that?

Not necessarily every possible combination. But probably I'd be adding new test if bug happens to sneak into production.

 
> The lessen here is: when you want to test for None, TEST FOR NONE. Don't
> use "or" when you mean "if obj is None".
Explicit test against None is good thing, but this is off-topic.

 
> Why would __init__ require fifteen arguments if the user can pass one
> argument and have the other fourteen filled in by default?
How would you fill a queue in default argument using database connection?


> The question here is, *why* is the create() method a required part of your
> API? There's no advantage to such a change of spelling. The Python style is
> to spell instance creation:
> 
>     instance = MyClass(args)
> 
> not 
> 
>     instance = MyClass.create(args)

In above case, probably no need.

If we are doing manual DI, like this:
instance = MyClass(args, deps)

then it might make sense to have
MyClass.create(args)

Code using the class won't need to deliver the dependencies manually. More on this later.


> You're not actually adding any new functionality or new abilities, or making
> testing easier. You're just increasing the amount of typing needed.

This is what I'm disagreeing with and would like to see answer or alternative equivalent suggestion.

Example class is easy to test, because I can inject there mock object in __init__ and only thing init does is assigns attributes to it's instance. It is massively better alternative than assigning to example._queue, as that would be subject to failing tests for no good reason. And things get worse when doing example._deep._deeper._deepest, when change of implementation in any of those nested dependencies will cause your tests to fail. Tests should fail when the behavior of your system changes (or is not what you expect).

How do you test above class without DI and without using privates in tests (break Law of Demeter).


> Of course it will. You are testing the create() method aren't you? If you're
> not testing it, how do you know it is working? If you are testing it, then
> it will be just as slow in the tests as it will be slow for the poor users
> who have to call it.

Sorry, I should say unit test here. I might ignore create in unit tests or do tests just to test the create, trying to mock dependencies, if it is possible without getting heart attack. Otherwise, I'd leave testing for acceptance tests, who would be using real stuff anyways and would be slow anyways. Idea is having tons of fast unit tests vs dozens of slower acceptance tests.

> you still have to test both cases, regardless of whether you are doing
> white-box or black-box testing. These are two separate APIs:
Agree, I'd just do it in different scope of testing.

> > @spec('queue')  # generates __init__ that populates instance with queue
> > given as arg 
> > class Example: 
> >     @classmethod
> >     def create(cls):
> >         return cls(Queue())
> 
> I don't think that this decorator example makes any sense. At least I cannot
> understand it. Why on earth would you write a decorator to inject an
> __init__ method into a given class? Unless you have many classes with
> identical __init__ methods, what's the point?

@spec('queue') would generate __init__(self, queue): self._queue = queue
@spec('queue', 'list') would generate 
__init__(self, queue, list): 
    self._queue = queue
    self._list = list
etc...

Similar mechanism is used in pinject, which is one of the DI frameworks to Python.

Br,
Teppo



More information about the Python-list mailing list