What I learned using unit tests/TDD
pauloortins.com
pauloortins.com
A much simpler and less nasty explanation could be that to people who do TDD, the difference between 'doing TDD' and 'writing good unit tests' is completely uninteresting. It's all part of the same engineering practice.
This blog post clearly seems to target people with an interest in TDD, so I don't see what's wrong with that.
Also, please drop the word "cultists." You can disagree with a technique on legitimate grounds without resorting to ad hominems. I'm downvoting you because you're not adding any substance to the debate.
I think you're relying on a misunderstanding of TDD to make your conclusion. You say that "people keep pretending the benefits of testing are benefits of TDD." While it's true that many practitioners confuse the two, that is not the purported benefits of TDD. It's not fair to draw a conclusion based on that premise.
You originally said, "TDD has no benefits," which is a definitive claim and not supported by evidence. That's vastly different from saying, "There is no evidence that TDD provides any benefit." You seem to be conflating being unable to draw a conclusion with drawing a negative conclusion. They're not the same thing.
> If you've read it, why do I need to re-show it to you?
Because the research I've read doesn't support your conclusion that "TDD has no benefits." I'd like to know if there's a body of research out there that I haven't read yet.
You seem to have an axe to grind with TDD salesmen. That's fair enough. Salesmen aren't in the business of advancing knowledge. They're in the business of persuasion. If you want to criticize salesmen for not being scientifically honest, be my guest. Sometimes, this tactic causes harm. For example, if a salesmen persuades someone to buy homoeopathic remedies instead of traditional medicine, people could die.
I think it's good that you're skeptical of the salesmen, because they're business doesn't require them to be scientifically honest. But that shouldn't inform your opinion of TDD (or any other subject), because there are others in the world whose business does require them to be scientifically honest. And they're the ones whose words you should read to form your opinion. And that is why I asked for sources.
No, you don't have to prove a negative. If it has not been proven, then it does not exist. Until TDD has proven benefits, it is correct to state that it does not have benefits. Just as until unicorns are proven to exist, it is correct to state that unicorns do not exist.
>Because the research I've read doesn't support your conclusion that "TDD has no benefits."
Yes it does, you just want to pretend that proof needs to work in reverse. It doesn't.
>But that shouldn't inform your opinion of TDD (or any other subject), because there are others in the world whose business does require them to be scientifically honest. And they're the ones whose words you should read to form your opinion
My opinion is based on the evidence, that's the point.
That's equivalent to saying that a scientific claim doesn't have to be falsifiable. That's not true. A claim has to be falsifiable for a reason.
> If it has not been proven, then it does not exist.
So the Higgs Boson didn't exist six months ago? It wasn't proven, because there wasn't enough evidence for a statistically relevant result until March 2013. So does that mean that prior to March, you would have concluded that the Higgs Boson doesn't exist?
If I had to sum up my problem with your reasoning in one sentence, it would be "belief is scalar, not boolean." Evidence should adjust your belief state somewhere between 0 and 1, but it should almost never be exactly 0 or 1.
> Just as until unicorns are proven to exist, it is correct to state that unicorns do not exist.
That's a variant of the "existence of god" problem. It's a non-falsifiable hypothesis. That doesn't fall within the realm of science, because it's not subject to empirical evidence.
> My opinion is based on the evidence.
I still haven't seen the evidence to support your conclusion.
TDD's claimed benefits are falsifiable. Much like the Higgs Boson prior to March 2013, they have been neither proven nor falsified. But that doesn't mean we can't form a tentative belief based on the weight of the current evidence available.
The weight of the evidence I've seen leans toward indicating some real benefits beyond normal unit testing. It's inconclusive because the studies lack statistical power and procedural consistency, but that doesn't mean we should automatically jump to a hardline decision in either direction (positive or negative). (Again, belief isn't boolean.)
No it isn't. Simply stating the opposite of reality doesn't make it so.
>It wasn't proven, because there wasn't enough evidence for a statistically relevant result until March 2013.
That is up to interpretation. Many felt there was enough evidence. This is not a case like that, there is literally no evidence at all for TDD. None. And you are demanding that I prove it doesn't have benefits. Prove programming while wearing pink socks doesn't have benefits.
>I still haven't seen the evidence to support your conclusion.
That is your fault, not mine.
>TDD's claimed benefits are falsifiable
And have been falsified, like in the link I provided you. But TDD salesmen use that falsification as evidence supporting it.
> there is literally no evidence at all for TDD
> And have been falsified
Falsification requires evidence.
You can't say there's no evidence and then turn around to draw a conclusion two seconds later.
> like in the link I provided you
That is but one blog post. Science doesn't work by cherry-picking results. That's a tactic used by climate change deniers and homoeopathic medicine supporters. The evidence must be taken in full.
In addition, I skimmed the paper cited in that article [1]. It appears to be measuring TDD's effects on external quality. That is not a purported benefit of TDD. Most of the research I've seen agrees with that paper: TDD doesn't improve external quality (defined by passing acceptance tests). The contention is over whether it improves internal quality. Also note that the paper cites "process conformance" as a threat to validity, which is what I've been saying is one of the major problems with TDD research all along [2]. The experiment also used students, which is known to be a problem when generalizing results. With TDD in particular, some research indicates that it's more effective with mature developers and ineffective with inexperienced developers. If you've read the rest of the research instead of cherry-picking, you would have known that.
I'm not trying to make a bold claim about TDD. The evidence doesn't support that. I'm trying to help people like you look at the subject objectively. You can't form an objective opinion without evidence (which you claim) or while cherry-picking evidence (which you've done). But frankly, I don't think you want my help, so the last word is yours if you want it.
[1] https://ieeexplore.ieee.org/application/mdl/mdlconfirmation....
[2] That was my first bullet point in my first comment on this thread: "there's little confidence that TDD was applied correctly in many of the studies."
My thought is that the general advantage of TDD is that you are thinking about your eventual implementation from the outside. You write tests in a different mindset based on expectations, rather than when you are implementing the production code where you are more concerned with the details of logic, etc. Unit tests also tend to be a bit more readable than the actual implementation logic, and so act as a nice form of documentation as a side-effect. In this way, test-first development provides a more gradual transition between a specification or design and the end implementation, which results in fewer missed requirements, allows for easy refactoring, etc.
This is of course only related to TDD however, not unit testing in general. Hence, it still irks me that many of my tests look so similar to the logic they are testing, and thus aren't really testing for correctness at all.
Like that you build your confidence layer by layer and with every set of tests you verify that you are still building on solid ground instead of quicksand.
That's why you test at the interface level, it allows you to swap out the implementation without having to re-work all the test code.
I'm not sure if you're in the same situation I was in, but let me tell you: My situation was an anti-pattern. All I was testing was that my code does what it does. I wasn't testing that my code was working.
Make sure you're not making the same mistake. If you are, a symptom would be that lots of tests fail when you're only refactoring.
The symptom of many tests failing during refactoring is something I'll be keeping in mind, however.
I watched a talk by Yaron Minksy the other day that also talked about the idea that you should "make illegal states unrepresentable" that was really good: http://vimeo.com/14313378 (he starts talking about that at around ~17:50)
First we create two differently typed tags for Red and Black.
data Red
data Black
Note this differs from something like data Color = Red | Black
in that Red and Black are distinct at the type level---the compiler will actually complain if one is used where the other ought to be.Now we state that Trees must have Black roots. This is done using a GADT (Generalized Algebraic Data Type) which allows us to build constructors of types which restrict some of the types.
data Tree a where
Tree :: Node Black n a -> Tree a
For instance, compare PoorTree which would let us make Trees with Red roots data PoorTree c n a = PoorTree (Node c n a)
It really does nothing more than wrap up a Node to hide it and change the interface (if you want).Now we need one last piece before digging in to the core. This is the existence of Type-level Naturals. The idea is that, like Red and Black, we introduce two new distinct types, one basic and one parametric.
data Zero = Zero
data Succ a = Succ a
which lets us write (very verbose) naturals like (Succ (Succ (Succ Zero))) == 3 and have the type reflect this exact value (Succ (Succ (Succ Zero))) :: (Succ (Succ (Succ Zero)))
(Note this is a little weird, since we can also form the value (Succ 'a' :: Succ Char) which doesn't make much sense. More powerful dependently typed features help here.)Now consider just a branch in the tree, we form that with a simple data type
data NodeH l r n a = NodeH (Node l n a) a (Node r n a)
which tracks the left and right colors in its type using the l and r type parameters. It also guarantees the constraint that both the left and right child trees have the same black-height. So, if (black3 :: Node Black (Succ Zero) Char) then (NodeH black3 'a' black3 :: NodeH Black Black (Succ Zero) Char). And if (black2 :: Node Black Zero Char) then (NodeH black3 'a' black2) is a compile time type error.So with all these pieces, we can encode the Red/Black criteria directly into our Nodes. Remember already that if we have a Tree it must have the first Node be Black. We can now construct new Nodes in three ways, each of these ways being constrained so as to enforce the balance rules.
data Node t n a where
-- If we construct a Node with Nil, it forms a leaf.
-- Not only are we not allowed to associate left and
-- right branches (we don't get to give it a NodeH type),
-- we're forced to set the numerical type parameter to Zero ~= 0.
Nil :: Node Black Zero a
-- If we construct a Node with BlackNode, we can pass a
-- subtree with the NodeH parameter, but the type of node we
-- get back is necessarily going to increase its height
-- since (Succ n) is like (n + 1).
BlackNode :: NodeH t0 t1 n a -> Node Black (Succ n) a
-- Or we can construct a Red node. In this case, our black-height
-- remains unchanged, but we can ONLY do so when we've got a NodeH
-- branch with two black subNodes.
RedNode :: NodeH Black Black n a -> Node Red n a
So, it's a type error to construct a tree in this format which doesn't obey the Red/Black balance rules. Any transformations you write on the tree will be rejected by the compiler if they break the rules. data Node = Nil | BlackNode | RedNode
but with the extra extra restrictions on the type variables.As weird as it is, I always recommend Agda/Coq for learning GADTs. It feels like overkill, but Haskell's DT stuff is really wonky, so it's nice to see GADTs expressed in a clean manner before trying them out in Haskell.
This is often conflated with type inference where the program code itself is analyzed in order to compute what type it "ought to be". Some combination of type checking and type inference is a powerful, fast, comprehensive tool for encoding how your program ought to behave alongside the program itself.
Elaboration:
With advanced type systems you can prove that your code satisfies (or fails to satisfy) any property that you can encode as types. The more expressive the type system, the more properties you can encode, and the more kinds of claims you can prove.
But there will always be some properties that cannot be proved or that are too expensive to encode within a given type system. If you care about these properties, you'll have to find some other way of making sure they hold. So, even with advanced type systems, you'll end up writing tests.
The flip side is that there are many important properties that you cannot adequately test for (e.g., "my code is free of injection vulnerabilities"). If you're not using proofs to make sure that these properties hold, there's little reason to believe that they do hold, regardless of how much testing you do. So, even with lots of tests, you'll end up using types.
You need both.
If those fail at any point it drives you to either use a more permissive type or to just test some useful cases. Not infrequently, test cases can be upgraded into more powerful types.
It's one thing to name a few example inputs and outputs of a single function, it's another thing to think about how a single function interacts with itself (such as what's needed to write a quickcheck property like "for all strings, `reverse . reverse == id`"), it's yet another thing to think about how collections of related functions interact (like seeing some Category laws), and it's a whole set of further things to see all that thoroughly enough to encode it in a logic system.
A common scenario I encounter is that sometimes when you are refactoring a method and refactoring it into multiple methods (for better readability or segregation); that you are required to make the new (sub) methods publicly available for testing purposes. This goes against the intention of improving readability because now you have to expose certain methods that you would rather haven't be called independently. In such way, TDD helps identify artifacts with too many responsibilities.
The best tip I can think of to give someone approaching/doing TTD is to make a check-list beforehand of what you are trying to achieve. This helps to focus your attention on usage patterns and identify 'end-points' (end points being testable artifacts). Just because you're doing TDD, doesn't mean you don't have to have a plan to start with.
It depends, we have to analyze each case and choose the advantages and disadvantages.
Strictly speaking, though, refactoring should never require new tests, unless you're specifically refactoring to increase testability. New tests should be related to specific bugs or feature requests.
I see the return values of certain functions and computations, and while debugging my failing test, I can see a path to the solution I need.
Once the test succeeds, it's time to clean up the ugly code and refactor it into clean subsections, be it functions or classes.
So yeah, TDD is a great tool. One of many in my toolbox.
That's also true for vanilla regression tests. There's nothing special that TDD adds in this regard.
Take a loot at http://www.ime.usp.br/~aniche/files/tdd-and-design-draft.pdf
What are we supposed to be looking at there? Like most TDD literature I see a lot of assertions, but no evidence to back it up.
Try looking at SEED http://evidencebasedse.com/ . TDD comes out not to bad. Quality correlates very well with # of unit tests regardless of methodology. TDDers tend to have more unit tests. Therefore TDD tends to produce quality code at better rates than other methodologies because it tends to produce more tests.
it’s almost impossible and you will lose too much time
but cover the majority of your code is perfectly
achievable. A good rule is test
*everything that can possibly break.*
but... but...[disclaimer: I run a company that does hosted CI: https://circleci.com]
But yes, I think that Continuous Integration is a good practice.