Testing code is simple
th3james.github.io
th3james.github.io
- this article only deals with unit testing
- it does not help you write more maintainable tests
For the record, I don't think (unit) testing is simple. Tests are, in my experience, the least maintainable and readable part of a codebase. You often end up with many lines of setting up complex business objects. And usually, the only help wrt to what the hell is being tested is the method name. And that's just unit tests. Integration tests are a major pain, and often even harder to understand and maintain.
I would say that 90% of the grief I have experienced building applications is the validation part - that you are actually building the right thing.
For me the right amount of automated testing is all about exploring the cost vs benefit boundary, which shifts with tooling and circumstance. But my firm rule now is: code must either have a firm expiration date or full test coverage. Nothing between is allowed.
So if you're going to cut back on testing, make sure your customers have agreed in writing on a date after which they should throw the code away.
Or when refactoring a piece of code that renders UI, write some tests to verify a couple of common cases and then refactor. Run the tests and I'll know whether the output is the same.
I've always said that RSpec is about documentation, not testing (hence the name - it's a specification for your code).
However, recently, I've been using Minitest-spec; not quite as readable but less magic and it feels faster.
1. Write your code so that independent units of functionality (algorithms) can be tested. 2. Make sure people understand, when changing code, either a) change the tests first, or b) recognize what tests should be failing because of the change and which shouldn't.
For good, testable code combined with good tests, it should be rare that the tests start failing when you change the implementation. Admittedly, that's true in theory and can vary widely in practice.
The appropriate practices there are short iterations, frequent releases, acceptance tests, and having product people sitting next to developers.
For those wondering how unit testing relates to other practices, go read Kent Beck's Extreme Programming Explained. He's one of the people who led the current wave of unit testing adoption via jUnit. Years before the term "Agile" existed, he was on a team that found a highly productive way of working, one for which unit testing was an important foundational element. But it was just one of 12 practices they saw as necessary.
Once you have that working well and you're building what the businesspeople expect, the next step is to make the businesspeople start testing their assumptions. That's basically what the whole Lean Startup thing, minus the fad, is about: let's not just go build whatever the guy with the tie says. Let's all get evidence about what's really worth building.
It's sort of like "the first dose is free", but applied for good.
1: small print - writing good tests is hard
I could likewise argue that "writing code is simple and easy, but writing good code is hard", but this statement has little value in itself. It seems more an argument against using complex testing frameworks and going for simpler syntax. As a fan of Nose, I absolutely agree.
The more complex your code, the more coupling there is, the more sources of bugs there are potentially... and the harder it is to write tests for that code.
Break down your code into smaller methods and more focused objects and just watch your unit tests get simpler.
Basically though, if you find your test difficult to write, there is a problem, and it's not with the test. And that should be your sign that you either don't understand the problem, or you are trying to do too much.
Now, this is hard. It's hard to accept that way of thinking. But the end result has always been cleaner, better code in my experience. It's not always the obvious way, but it's the best way.
The second problem is that whenever you're faced with a large, older codebase, it's often difficult or impossible to refactor away complexity in core components.
Currebtly I am only toying with it in one of my pet projects but it seems promising.
If your unit tests are hard, that's a non-SOLID smell.
Also, 99% of the time, the first test you are going to write in a job is for an existing piece of software. So it will definitely not be easy. But you will learn so much more about the software you are writing the test against rather than writing code in the edit-and-pray methodology.
I highly recommend reading http://www.amazon.com/Growing-Object-Oriented-Software-Guide...
should probably be written
"Using test testing in development is now widely recognised as ‘a good thing’"
There is controversy around specifically TDD and studies showing that it does no better than writing unit tests during or after development. For citation see the references at the end of the chapter on TDD in "Making Software: What Really Works, and Why We Believe It"
The other reason TDD is good is that it forces you to make modular code with limited functionality to make it easy to test. You can do that without TDD... but TDD forces you to think about it ahead of time, rather than writing a huge mess of code and then saying "well, this will be too hard to test, so we won't bother".
TDD isn't magic, it's more of a mental hack to get you to do the right thing. If you do the right thing anyway, then no, TDD won't help.
Note, I'm not a huge TDD guy. In fact, I usually write my code first. But I've done some TDD and I can definitely see the benefit, and I try to do it when I can.
If you're lucky, you're doing real QA, where an smart, knowledgeable, and independently minded QA analyst can discover and raise such concerns before the work gets done.
"Testing" is a shithole because many (most?) shops don't give it much respect -- nor resources.
P.S. One of the shorter meetings of my life. 5 minutes, including the socializing. The technical part took maybe 2.
So, what is the purpose of unit tests?
1. When writing tests before the code, the tests can help you wrap your head around what the code should and should not do.
2. Forces distinct functionality/algorithms to be broken out into their own functions for easier testing.
3. Forces algorithms to be broken away from other, less testable, code (ie, read from data base, algorithm, write to database... the piece in the middle there should be in it's own function for testing)
4. When refactoring, gives (more) confidence that the changes being made aren't breaking anything. The corollary to this is it makes refactoring more common because you can feel braver about doing so.
5. Proves the code is doing what you say it's doing (with the tests). Note that this doesn't mean the code is right, just that it isn't wrong in the ways you're testing. Learning to write tests with good coverage and how to partition input data for that is a skill learned by experience.
6. Documents exactly what the code does for someone that comes along later. For much code, this can be determined by looking at the code... but in many cases the tests can say it cleaner.
7. Documents what the code DOESN'T do, and this is an important one that can't really be understood by just looking at the code. If the tests don't say the code does something, it could change later. If the tests only exercise negative number inputs but the code happens to do something useful with positive numbers... user's shouldn't rely on the positive number behavior.
8. Confirm that, when a bug is found, the fix actually fixes it. When finding a bug... a) find a way to reproduce is, b) write a test that does so and fails, c) fix the code and see the test pass... d) the bug never returns without being caught by the test.
* I'm of a functional persuasion, I like to think about types before code, I already tend break things up into testable components (also known as pure functions). What benefits might I find by adopting TDD?
* I understand the there are different benefits to be had from the act of writing tests and the artefact that is produced (ie the test suite). Do you the expect the tests that you write find bugs immediately? Or does the artefact only help to document code/detect regressions?
It also makes sure that if code changes, it's still functioning correctly. Aside from catching initial bugs, this is the biggest addition to maintainability of code.
I find unit tests to be incredibly liberating. Have you ever made changes to some parts of the code, but then worried you might have broken something? With unit tests, you can be confident that your change didn't break any expected behavior.
* To quote Dijkstra, "Testing shows the presence, not the absence of bugs", so presumably, you write tests to build confidence rather than to ensure "that what you think the code does is what it is actually doing". Is this accurate?
* If so, suppose you given a code-base that contains tests. How do you determine how much confidence to place in said software?
* It's very common for the space of possible inputs to be infinite, so you can't possibly test every case. How do you decide which ones are useful to write tests for?
* What is your response to Rich Hickey's depiction of, what he calls, guard-rail programming ("Simple Made Easy" from around 15:30 onwards)?
* Finally, how do know when you've written a good test suite? It's obviously good if it finds bugs, but how do you know that it is good without running it?
Sorry to bombard you, I'm asking because I'm genuinely interested and I want write more robust software.
> Have you ever made changes to some parts of the code, but then worried you might have broken something? With unit tests, you can be confident that your change didn't break any expected behavior.
I must say, I've done every permutation of changing code with/without tests where my changes have broken/have not broken things. Subjectively, I can't say that there has been noticeable correlation in any direction. Perhaps that's because I'm bad at writing tests, but then, how do I write good ones?
I think the real value of tests is exposing expected behavior to teammates, and then providing quick sanity checks against that expected behavior.
The biggest thing new developers miss about testing is that most times you're not writing tests for yourself, you're writing them as a courtesy to your teammates.
Learning to recognize potential edge cases and partition inputs to check as many input categories as possible is a skill learned through experience testing.
* You write tests in accordance with a pattern:
1. Arrange
2. Act
3. Assert
4. Annihilate
If you annihilate your test data with configuration options, and you abstract your "arranging" into factories and custom methods, then every test you write should be exactly three lines. When I learned this it gave me a consistent mental framework to think about how to write good tests.
* Test the interface not the implementation:
This was an eye opening realization about what to test. If you haven't seen it yet, watch Sandi Metz' incredible RailsConf talk - http://www.youtube.com/watch?v=URSWYvyc42M - it's so good
* Stubs:
Stubs are exactly like the viruses' that we learned about in biology class. You load them up with a value then they literally latch onto a method and inject that value into it. When you're just starting out with testing, effective use of stubs can get you really far, and only when you start hitting the limits of practicality is when you need to think about bringing in something more complicated.
* Testing isn't easy per se, but it is definitely enjoyable if you focus on making it that way
* Cucumber is pretty stupid, don't waste your time with it
xassert(condition,value);
e.g., xassert(result==3,result);
When the assert fails, the value is also reported. Helps with the question, "So it's not 3. What is it?"
I started using xassert when working in embedded systems where there is no debugger, no stack trace. Knowing the value of the error is valuable.
retcode = some_os_function(blah,blah,blah); xassert(retcode==OS_SUCCESS,retcode); <--- why did my call fail?
For example: What do you do if your function has a call to a 3rd party webservice? You do a mock.
What do you do if you have 37 levels of state that are dependent on the objects you are receiving from the mock? Create 37 different business objects and verify that the output for each is correct.
This problem builds and gets bigger, harder, and more conceptually complex. So while it can be easy to get started, it is not by definition intrinsically easy.
However, it is well worth every minute spent. It makes refactoring and building up much much easier. It also leads a bread crumb of clues to figure out what the code is doing. It leads to better code stability, and far less regression.
Buuuuut, a huge complex app is probably not the right place to try learning any new technology. Do a few small things first, prototype something out, test it along the way. Then maybe something a little more complicated. Slowly expand your horizon of uncertainty.
That way you only need to focus on learning one thing at a time. The assert() call that OP suggested is exactly enough to start thinking about testing simple code; once you're doing well at it, you can start noticing repetition and opportunities for abstraction (and finding existing libraries that have these abstractions already done for you); then, you start doing testing on more complex code and start learning about mocks to facilitate testing there.
Some people do really well in a "big bang" kind of learning environment, but I definitely don't. For me, learning works best given: a rough map of where I'm trying to get (even just a mind map that gives me things to look up when I get stuck), and an opportunity to master each piece before expanding onto new topics.
I am constantly confused by this idea that because testing can get hard and complicated we should not do it. It's as if to say we should trust our untested code purely because the testing would be more complicated than not testing.
They don't see the benefit for themselves, but like proper documentation, it's mainly a courtesy to other developers.
What I wish I was told about testing is that if you modify your workflow, it benefits YOU as well as your colleagues. Without testing, trying out a feature means:
1. Make changes. 2. Restart application or server (possibly refreshing page). 3. Go through a series of clicks or keystrokes. 4. Didn't work? Go to step 1.
Steps 2 and 3 can take a lot of time, and you'll likely be doing it over and over again (boring!). With testing this becomes a 2 second cognition-free process, and even that can be automated with Grunt/gnotify notifications. The boring part of coding is now automated. With good coupling, you can quickly isolate your change/compile/run cycle to the part of the code you're working on.
Programmers already know the benefits that testing brings 6 months down the line when something breaks, but it's wishful thinking to expect people to think that far ahead (and smaller companies are often in a hurry to get something out the door before the money dries up). If you want intrinsic motivation to write tests, you learn how to work such that tests provide immediate benefit.
I like to imagine a world where automated analysis supplements testing by providing a middle ground of "moderate gain + no programmer effort" (if you're interested, http://bugchecker.net)
In fact, in browser js you can even wire onerror to send error server to a server (eg google analytics) in production.
What's the point of all the fancy .isEqualTo etc!
E.g., "assert(a == 3)": "Assertion failed."
vs.
"assertEquals(a, 3)": "Expected 3 but was X."
The fancy "spec" style ("a.should.be.equal.to(3)") is overkill, though, I'd agree. It adds complexity for dubious benefit.
assert("a === 3", a, b)
And then it can say "a === 3 failed. (2, 4)"
I don't want.to.sitThere.writingThis(kindOf, crap)
The code reads worse for a number of reasons:
:: you've written an equality, rather than truth, assertion named "assert" rather than "assertEquals".
:: you're writing what would be the code of a truth assertion as a string for the message test.
For the same role, most unit testing frameworks have an equality assertion / expectation assertion that serves essentially the same role but reads better.
> I don't want.to.sitThere.writingThis(kindOf, crap)
Then don't use spec-style frameworks. There's plenty of xUnit and similar style frameworks that use simple assertions (and, yeah, you can just use bare truth assertions all the time if you want, but you've usually got a handful of nice tools for common cases like equality assertions to go with them.)
function testCase() {
// do some stuff
assert(a>=b, 'less pictures than participants', a, b);
// do some stuff
assert(cool.length, 'cool is empty');
}
output: "testCase: less pictures than participants (3, 4)"
"testCase: cool is empty"
easy, no?However, the beneficial side effects of TDD are much greater than just the verification aspects so I think more foundational work needs to be done on test frameworks for hard problems.
Test-Driven Development for Embedded C http://pragprog.com/book/jgade/test-driven-development-for-e...
Let the tests actually shape your workflow, and then get back to me about how your development is "test-driven".
Not that they're not hard to write yourself of course.
I only write more tests if there are important business rules (new employees should be created with "Pending" status), or to cover edge cases uncovered by QA.
theres no talk about clean states between tests. no concern about emulating data sources. etc.
this is what should be called half assed functional tests. not even unit tests as everyone here says.
man tests really are black magic around those parts...