神刀安全网

One weird trick that will change the way you code forever: JavaScript TDD

This is a presentation delivered to the Squiz Melbourne Engineering team. It repeats some of the material I’ve covered in other posts. So apologies if you’re a regular reader and you’ve heard all this before.

Introduction

One weird trick is a cheesy title, I know. Originally I was using it as a draft placeholder title for a joke. But the more I thought about it, the more it seemed appropriate because it’s true. Test Driven Development is one weird trick that will change the way you code forever (in a good way). I will explain why as we go on.

I’ve broken this talk up into three parts:

  • Why practice TDD?
  • What is TDD? and
  • How do you practice TDD?

In the how section I’ll work through a step-by-step example of how to write a single function with TDD. It will be very brief, because I don’t have a lot of time, and I just want to give you a flavour of how TDD works. For now though, let’s start with the why .

Why?

I want to think back to some of those ‘oh cr*p’ moments in your coding career.

  • Have you ever fixed a bug, only to find that it broke something horribly in another part of the system? And you had no idea until the client called support in a panic?
  • Have you ever been afraid to touch a complicated piece of code for fear that you might break it and never be able to fix it again? … Even though you wrote it?
  • Have you ever found a piece of code that you’re pretty sure wasn’t being used any more and should be deleted? But you left it there just in case?
  • Have you ever felt like your code was a tower made of soft-spaghetti, held together with Clag glue and wishes?

If you haven’t, then you probably don’t need TDD. Or you haven’t been coding for very long.

What if all of these could be a thing of the past? Imagine going back to some old code and thinking “Actually, this code isn’t too bad. It feels clean. I know what’s going on. Whoever wrote this was a genius!”

Sounds like unicorns and rainbows, right? But bear with me. I really do want you take a moment and imagine what that would feel like. What would it be like to come back to some of your own code, months (or years) later and not have that “Ewww” reaction? How would it feel to be able to fix a bug and know for sure that it had been fixed, and that you didn’t break everything doing it? Imagine surprising yourself with some of the elegant coding solutions you write.

I know that sounds a bit dramatic and cheesy, but it is possible. It’s a side-effect that I wasn’t expecting when I started using TDD, but it’s something I’ve actually experienced. There are some projects I look forward to working on again because I know the code is clean and organised.

Excuses

Now, you may have heard of TDD before. And maybe you thought “Ah yes, testing. That’s definitely something I should do.” And then you didn’t do it. Anyone?

I hear that all the time. I think there are two reasons why:

  1. The first reason is that testing seems like an optional extra—gold plating; a nice-to-have. You don’t need the tests to have working code. And what’s the first thing to get dropped when a project starts getting behind? Those ‘superfluous’ tests, right? Why waste time on something that isn’t absolutely essential to getting the project completed?

  2. The second reason we don’t practice TDD more often (I think) is because of the word ‘test’. Testing sounds tedious; boring; time-consuming. You’re under the pump and you’ve got to get this project out the door. You don’t have time to write tests on top of everything else that has to get done. It seems like a nice-to-have. It’s like doing your taxes—you might understand that it’s important, but it’s definitely not sexy or fun.

I felt the same way about TDD. But so many smart people seemed to be saying it was a good idea that I reluctantly gave it a go. And eventually I discovered a secret:

Test Driven Development is not about testing.

Did I just blow your mind? Let me elaborate a little:

Test Driven Development is not about testing. It is a way of thinking and coding that just-so-happens to involve tests.

What do I mean by this? What is it about then, if it’s not about the tests?

TDD is a technique that gives you confidence in your code. It’s a life-hack. It’s not really about the tests. Those are just a useful side-effect. The real benefit of TDD is the way it teaches you to think about code, and the confidence it gives you to know that your code definitely works.

More excuses

Doesn’t TDD slow you down and make you less creative?

The short answer is no. Yeah, TDD seems slower at first. And when you start out it does take more time as you get used to id—just like any new skill. But as you go on it starts saving you more and more time. This is because you spend less time figuring out why things are broken and more time getting things done.

In turn, spending less time bug-hunting gives you more time for creativity and refactoring. If you’re practicing TDD properly, it encourages you to try the stupid-simple dead-obvious thing first, and see if it works. It allows you to try things with less risk of blowing everything up.

And one more thing before I go any further:

Test Driven Development is not the same thing as unit tests. Unit tests are a type of test. TDD is a coding technique.

In our organisation, we have a bad habit of referring to TDD as ‘unit testing’ (and I’m just as guilty as anybody). But they are not the same thing. Unit testing is a particular type of test that we use frequently for TDD (hence the confusion), but it’s not the only type of test. I’m trying really hard to stop using the two interchangeably, so if I do, please let me know.

But if TDD is not about tests, and it’s not the same as unit testing, what is it, exactly?

What?

TDD is a technique for writing code where you write a test before you write any ‘proper’ code. But that’s just the single-sentence summary. In the book Test-Driven Development By Example , Kent Beck explains that TDD has two simple rules that imply three simple steps. The rules are:

  1. Write new code only if you first have a failing automated test.
  2. Eliminate duplication.

And the three steps follow on from the two rules:

  1. Red —write a little test that doesn’t work, perhaps doesn’t even compile at first
  2. Green —make the test work quickly, committing whatever sins necessary in the process
  3. Refactor —eliminate all the duplication created in just getting the test to work

These steps are fairly simple, but when followed they produce some powerful results, so long as you areusing your brain. As I said, the real value is not in the tests themselves, but in the way it teaches you to think about coding, and the confidence it gives you in your code. To show how that works, we’ll run through a very short example:

How?

Imagine we’re going to create the following application:

One weird trick that will change the way you code forever: JavaScript TDD
The Pugs of Flickr Web Application

All it does is connect to the Flickr API and find the latest pictures of Pugs. I’m not going to run through building the whole application, but just a single step. We will pick one function from one module and build just that. (If you’re interested I’ve written out a step-by-step tutorial for building the whole application with TDD ).

So, before we do anything, let’s set up the project. First we’ll need a folder to work in, so let’s create that:

cd /path/to/my/projects/folder mkdir pugs-of-flickr cd pugs-of-flickr 

Next we’ll install Mocha , the testing framework we’ll be using (if you don’t have it already). And we’ll install Chai locally–a module that helps write assertions in a more readable fashion. (Assertion is just a fancy name for the bit that does the actual test, as opposed to all the setup stuff ):

npm install -g mocha npm install chai 

Then, we create a file for our tests:

touch flickr-fetcher-spec.js 

The file name is just the name of the module with -spec added on the end.

In my file I set up my very first test as follows:

// flickr-fetcher-spec.js /*eslint-env mocha*/ var expect = require('chai').expect;  describe('FlickrFetcher', function() {     it('should exist', function() {         expect(require('./flickr-fetcher')).to.be.defined;     }); }); 

This test is super-simple. It does nothing other than check that my module exists. That’s it. The describe() function says “I’m starting a new group of tests here”, and the it() function says “Here’s one test”.

So, I run my test suite like so:

mocha -R nyan ./flickr-fetcher-spec.js 

…and we get a sad cat. We have completed Step 1—Red. This is good news, because it means I can move forward. So, step two is to make the test pass. What’s the simplest possible thing I can do to make that test pass?

The simplest thing is to create the module:

// flickr-fetcher.js module.exports = {}; 

I run my test again… and I have a happy cat. Step 2—Green is complete. So we’re up to the refactoring step.

Is there any duplication going on here? Not yet. Is there anything I could do to improve the code? Maybe. I’ll tweak things just a little:

// flickr-fetcher.js var FlickrFetcher = {};  module.exports = FlickrFetcher; 

This makes it a bit clearer what’s going on without adding any new (untested) functionality.

And I run my test again… and the cat is still happy. So we’ve completed Step 3—Refactoring .

Let’s do something a little bit more useful (and more instructive). The Flickr API gives us photo data in JSON form. It doesn’t give us URLs for the images (because we have to tell it what size we want). So, we need a function that will take a photo object and transform it into a URL. Photo objects look like this:

{     "id":       "25373736106",     "owner":    "99117316@N03",     "secret":   "146731fcb7",     "server":   "1669",     "farm":     2,     "title":    "Dog goes to desperate measure to avoid walking on a leash",     "ispublic": 1,     "isfriend": 0,     "isfamily": 0 } 

We want a URL that looks like this:

https://farm2.staticflickr.com/1669/25373736106_146731fcb7_b.jpg 

The Flickr API documentation describes the way we make the transform using the following template:

https://farm{farm-id}.staticflickr.com/{server-id}/{id}_{secret}_[mstzb].jpg 

So, that gives us enough information to write a test:

// flickr-fetcher-spec.js /*eslint-env mocha*/ var expect = require('chai').expect;  describe('FlickrFetcher', function() {     it('should exist', function() {         expect(require('./flickr-fetcher')).to.be.defined;     });      var FlickrFetcher = require('./flickr-fetcher');      describe('#photoObjToURL()', function() {         it('should take a photo object and return a URL', function() {             var input = {                     id:       '25373736106',                     owner:    '99117316@N03',                     secret:   '146731fcb7',                     server:   '1669',                     farm:     2,                     title:    'Dog goes to desperate measure to avoid walking on a leash',                     ispublic: 1,                     isfriend: 0,                     isfamily: 0                 },                 actual   = FlickrFetcher.photoObjToURL(input),                 expected = 'https://farm2.staticflickr.com/1669/25373736106_146731fcb7_b.jpg';             expect(actual).to.equal(expected);         });     }); }); 

This just passes the example photo object into the new function, then checks that the actual output matches what we expect. Most of your tests should look roughly like that. You define an input , the actual value, and the expected value. Then you check to see if the actual result matched what you expected.

Let’s run the test… sad cat (red). So, we can write some code.

Now, what’s the quickest, simplest, easiest way to make this test pass? You guessed it: Return the URL we expect.

// flickr-fetcher.js var FlickrFetcher = {      photoObjToURL: function() {         return 'https://farm2.staticflickr.com/1669/25373736106_146731fcb7_b.jpg';     } };  module.exports = FlickrFetcher; 

Run the tests again, and… happy cat. That’s it. Test passes, we’re done. But let’s pause a moment and talk about what we just did there: Creating an almost useless function that still passes the test. This was the part I didn’t understand when I first started practicing TDD. You write only enough code to make the test pass. No more.

And it’s really hard. This is the main reason why it feels like TDD slows you down. It takes a lot of discipline to only write the bare minimum code. If you’re like me, you just know how to write the code, and have all sorts of ideas for making it super-efficient and elegant. But there’s no point in writing more code than you have to. Doing TDD right means restraining yourself and only writing enough code to make the test pass.

Let’s keep going…

This function is not complete. What happens if we pass a different photo object? Let’s find out… by writing a new test.

// flickr-fetcher-spec.js describe('#photoObjToURL()', function() {     it('should take a photo object and return a URL', function() {         var input = {                 id:       '25373736106',                 owner:    '99117316@N03',                 secret:   '146731fcb7',                 server:   '1669',                 farm:     2,                 title:    'Dog goes to desperate measure to avoid walking on a leash',                 ispublic: 1,                 isfriend: 0,                 isfamily: 0             },             actual   = FlickrFetcher.photoObjToURL(input),             expected = 'https://farm2.staticflickr.com/1669/25373736106_146731fcb7_b.jpg';         expect(actual).to.equal(expected);          // Second test with a different object.         input = {             id:       '24765033584',             owner:    '27294864@N02',             secret:   '3c190c104e',             server:   '1514',             farm:     2,             title:    'the other cate',             ispublic: 1,             isfriend: 0,             isfamily: 0         };         expected = 'https://farm2.staticflickr.com/1514/24765033584_3c190c104e_b.jpg';         actual   = FlickrFetcher.photoObjToURL(input);         expect(actual).to.equal(expected);     }); }); 

Run the test again… and it fails, as expected. So… what’s the simplest, shortest way to make this test pass? Yep. An if-statement.

// flickr-fetcher.js var FlickrFetcher = {      photoObjToURL: function(photoObj) {         if (photoObj.id === '25373736106') {             return 'https://farm2.staticflickr.com/1669/25373736106_146731fcb7_b.jpg';         }         return 'https://farm2.staticflickr.com/1514/24765033584_3c190c104e_b.jpg';     } };  module.exports = FlickrFetcher; 

We run the test again, and… happy cat (green). Are you getting frustrated yet? Don’t you just want to get in there and write the whole function? Bear with me, and think about the next step—refactoring. Could this code be any more efficient to past these tests? Well, no, not really. But the next question is very important. Is there any duplication here? …

Actually, yes, there is. But just to drive the point home, let’s add one more test.

// Third test with a different object. input = {     id:       '24770505034',     owner:    '97248275@N03',     secret:   '31a9986429',     server:   '1577',     farm:     2,     title:    'Some pug picture',     ispublic: 1,     isfriend: 0,     isfamily: 0 }; expected = 'https://farm2.staticflickr.com/1577/24770505034_31a9986429_b.jpg'; actual   = FlickrFetcher.photoObjToURL(input); expect(actual).to.equal(expected); 

Run the tests again… and sad cat (red). We can write some code. What’s the quickest, simplest way to get this code to pass then? Yep, another if-statement. Remember, we’re “committing whatever sins necessary in the process” to make the test pass:

// flickr-fetcher.js var FlickrFetcher = {      photoObjToURL: function(photoObj) {         if (photoObj.id === '25373736106') {             return 'https://farm2.staticflickr.com/1669/25373736106_146731fcb7_b.jpg';         }         if (photoObj.id === '24765033584') {             return 'https://farm2.staticflickr.com/1514/24765033584_3c190c104e_b.jpg';         }         return 'https://farm2.staticflickr.com/1577/24770505034_31a9986429_b.jpg';     } };  module.exports = FlickrFetcher; 

If we run the test again, the cat is happy (green). So, we’re up to the refactoring stage.

Now, do we have duplication going on?

Heck yes!

Let’s refactor:

// flickr-fetcher.js var FlickrFetcher = {      photoObjToURL: function(photoObj) {         return [ 'https://farm',             photoObj.farm, '.staticflickr.com/',             photoObj.server, '/',             photoObj.id, '_',             photoObj.secret, '_b.jpg'         ].join('');     } };  module.exports = FlickrFetcher; 

Now, isn’t that much nicer? Does it work? Let’s re-run the tests… …and happy cat (green).

Let’s savour that for a moment. We have some nice efficient code, that we know works, because we’ve got three separate tests verifying it.

But, we’re not done refactoring yet… do we still have duplication going on? Yep. There’s a whole bunch of it in our tests. So let’s refactor those:

describe('#photoObjToURL()', function() {     it('should take a photo object and return a URL', function() {         var testCases = [             {                 input: {                     id:       '25373736106',                     owner:    '99117316@N03',                     secret:   '146731fcb7',                     server:   '1669',                     farm:     2,                     title:    'Dog goes to desperate measure to avoid walking on a leash',                     ispublic: 1,                     isfriend: 0,                     isfamily: 0                 },                 expected: 'https://farm2.staticflickr.com/1669/25373736106_146731fcb7_b.jpg'             },             {                 input: {                     id:       '24765033584',                     owner:    '27294864@N02',                     secret:   '3c190c104e',                     server:   '1514',                     farm:     2,                     title:    'the other cate',                     ispublic: 1,                     isfriend: 0,                     isfamily: 0                 },                 expected: 'https://farm2.staticflickr.com/1514/24765033584_3c190c104e_b.jpg'             },             {                 input: {                     id:       '24770505034',                     owner:    '97248275@N03',                     secret:   '31a9986429',                     server:   '1577',                     farm:     2,                     title:    'Some pug picture',                     ispublic: 1,                     isfriend: 0,                     isfamily: 0                 },                 expected: 'https://farm2.staticflickr.com/1577/24770505034_31a9986429_b.jpg'             }         ];         testCases.forEach(function(t) {             var actual = FlickrFetcher.photoObjToURL(t.input);             expect(actual).to.equal(t.expected);         });     }); }); 

Now our tests are nice and clean too. We run them again and we still have a happy cat (green). Everything is nice and tidy and efficient.

Final thoughts

I’m hoping after this that you’ll give TDD a go. But I have one final word of advice: Start small . Don’t try and do everything at once. Pick one small, easy bit of a project and do TDD with that. If it’s easier to set something up in a Code Pen , then do that.

Once you’re comfortable with the three steps, then start thinking about how you can bring more stuff into tests. Think about how to restructure your code to make it easier to test. Slowly, all your code will start to improve. And, as you practice, you will become a better developer because you will learn to see the code differently.

More resources

I’ve written about TDD before on my website. There’s a step-by-step tutorial and some advice on where people get stuck:

If you’d prefer advice from someone who isn’t me, check out Eric Elliot’s helpful articles:

Or Rebecca Murphey:

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » One weird trick that will change the way you code forever: JavaScript TDD

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址