What’s the hardest thing in test-driven development or unit testing in general?

Writing tests!

The syntax or the tools aren’t the problem you can learn those enough to get started in 15 minutes.

The problem is how to take some vague idea in your head about what you want to do, and turn it into something that verifies some function works… before you even wrote the damn thing!

People tell you to use TDD. But how can you possibly write a test for something that doesn’t exist? I don’t even know what the function is going to do yet or if I actually want two functions instead of one and instead you want me to think of a test for it? Are you crazy?

How do all those people who tell you to use TDD do it?

That’s the thing test-driven development requires thinking of your code in a different way. And nobody ever tells you how to do that.

Until now.

How to take a vague idea and turn it into tests

Let me walk you through a system that you can use to do just that take any kind of idea for what you want to do, and turn it into something tangible, and thus, testable.

I use test-driven development a lot. This is based on exactly the kind of thought process that I go through as I write tests for my code.

This isn’t going to be about the technical details, such as “red-green-refactor”. Rather, I want to focus on the thought process that goes on when a seasoned developer writes code with TDD, and which makes it easy to do.

Remember: All it takes is adjusting your thinking. Yes it might take some conscious effort at first, but once you do it enough, it becomes routine. Just like writing loops and conditionals, writing tests becomes something that you just do without having to think about it a lot.

Let’s start with our first example: Calculating password strength.

As I started writing this article, I just pulled that idea out of a hat. I’ve never written code to do that until now I’m going into this as blindly as you would go into writing any real code.

Before we go into the process, there’s one important thing to know.Perfection isn’t the goal! Test-driven development is an iterative process, meaning you work in small repeating steps. Yes, we want to make a good educated guess, but it doesn’t have to be exactly right. Don’t get stuck thinking of some tiny detail, because in software, things always change . One of the great things about TDD is that it makes change easier, so if we don’t get this 100% right on the first try, we’ll just do it again. And that’s exactly how it should be!

Step 1: Decide the inputs and outputs

We start this process from a high level. We don’t care about the implementation just yet.

We have the goal: Calculate password strength. To get to that goal, we usually need some inputs… and then, we get some output based on them.

When you normally start writing code to get to some goal, you’d probably start with a function. You’d probably think of what data the function needs to work, and what kind of results it will give back. We can start this process by doing exactly that we just won’t write any code for it yet.

So how would this work?

The input is easy: It has to be the password. The output is also easy: It has to be some value describing the strength of the password. To keep things simple, let’s say the password is either strong or it isn’t so we can use a boolean value for the output. Step 2: Choose function signature

Now that we know what data goes in and what comes out, we need to choose the function signature that is, what parameters the function takes, and whether it returns something.

This step is again similar to how you would approach writing code without TDD. Before you can write any code for a function, you need to decide what its parameters and return values are.

First, the parameters. What does our function need to work? In this case, it’s simple all it requires is the password. We can do the whole calculation based entirely on that value, and that value alone.

What about the return value? Simple, since this is a calculation, we can return the result directly. In some more complex cases, the return value might be a promise. Or, instead of returning a value, the function might take a callback parameter or it might just not return anything at all.

Either way, at this point, we can now decide what calling the function would look like in code:

var strong = isStrongPassword('password string goes here');

Step 3: Decide on one tiny aspect of the functionality

We now know the goal, the data involved and the function signature.

In a non-TDD workflow, you’d jump into writing code for the function now. You might already have some ideas on how this would work we need to check for this, we need to check for that, the return value is affected by X…

This is where most people run into trouble with TDD. Your head is filled with all these ideas on how to write the function… but you’re not sure of exactly how to lay out the code until you start writing it.

Instead of thinking of all the choices… let’s focus on one tiny thing only.

What is the simplest possible behavior that we need to get a tiny bit closer to our goal?

A common problem is to try and tackle a chunk of behavior that’s really big. If we think of password strength, there’s ideas of different rules like special characters, numbers, password length, etc… Of course it’s hard to think of a test that would cover all of that!

So what’s the simplest possible step we can take to make this function be closer to the ultimate goal of validating a password?

What would be the very first line (or two) of code you would write if you built this function without TDD?

What is the smallest amount of code we can add to bring the function closer to working?

The very simplest rule for password strength might be the empty password. That’s really easy the output should always be false when the password is empty.

Step 4: Implement test

And just like that, we’ve arrived into implementing the test. I hope that was easier than you expected :)

Notice how all of the previous steps were actually similar to writing code without TDD?

The main difference is that instead of focusing on implementing the function, we’re focusing on how the function would be called, and what happens as a result. That is we’re thinking about how the function behaves under some conditions.

How the function behaves is what we want to test. Once you start testing behavior under some conditions (such as certain parameters, time of day, whatever), testing becomes a lot easier, because we can look at behavior from the outside. We don’t need to know the implementation if we’re just choosing behavior.

We decided the function takes a password as its only parameter. We also decided it returns a boolean to indicate whether the password was strong or not.

We also chose that for an empty password, the result should always be false to indicate an empty password is weak.

Let’s plug all of that into a test:

describe('isPasswordStrong', function() { it('should give negative result for empty string', function() { var password = ''; var result = isPasswordStrong(password); expect(result).to.be.false; }); });

Notice that we easily wrote that without knowing what the exact lines of code in the function are going to be. We decided that given an empty string as a parameter, the result should be false. One simple behavior, which easily translated into a test.

Step 5: Implement code

Very self explanatory. We’ll just add the smallest amount of code that makes the test pass.

function isPasswordStrong(password) { if(!password) { return false; } }

If we were to continue developing the password strength function, all we do is just repeat this. We’ll go back to step 3, and choose the next tiny step to take. Step 4, add test. Step 5, implement. Repeat.

If you keep advancing in small steps like this, TDD suddenly becomes a lot easier. Yes you might end up with several tests for a fairly small amount of code, but that’s not a bad thing. TDD helps you in this way to reduce the amount of useless code you might otherwise write, because every line of code you add is verified by a test.

A more elaborate example

Can this system really work for more complex problems? It seem really simple doesn’t it.

Spoiler alert: The answer is yes, it can!

Let’s take a look at a slightly more elaborate example, so you can get a better intuition on how the system would work for something which has more moving parts.

What would be suitably annoying to test?

How about a debounce function? The idea with a debounce function is that it ensures some other function doesn’t get called if it has already been called within a certain amount of time. This is convenient for example if you need to handle scroll events, as typically you would only want the event handling to trigger once the user stops scrolling.

Since this involves time, it should provide a bit more challenge for the 5 step method I laid out.

Let’s start at step 1 again. What are the inputs and outputs of a debounce function?

Since the goal is to create a version of an existing function which doesn’t get called except some amount of time later, the first input should probably be a function. The second input can be the amount of time we want to debounce it.

As its output, the debounce function needs to return the delayed version of the original function, so that it can be called.

Into step 2: Function signature. We’ll pass the two inputs as parameters into the function, and it returns a new function. Simple.

So we end up with something like this:

var delayedFunction = debounce(targetFunction, delayInMilliseconds);

Now the more interesting parts… with step 3, we need to choose a tiny part of the function to implement. There are many possible parts to debounce: There’s a delay, if we call the returned function multiple times, it shouldn’t get called.. unless we call it with a long enough break… etc.

But let’s try to find the simplest thing to start with. If we call the delayed function returned by debounce, it should wait some amount of time, and then run the original function. I think this seems like a suitable place to start from.

And we’re in step 4 already. What would the test for this look like?

Same as before, let’s start by plugging what we chose into a test:

describe('debounce', function() { it('should call returned function after delay passes', function(done) { var delay = 5; var targetFn = function() { done(); }; var delayedFn = debounce(targetFn, delay); delayedFn(); }); });

We know that we need the delay in milliseconds, so we’ll start by that. We also need the target function. Since we know the target function needs to be called after the delay, we can use this for a quick and easy way to verify the test passes by calling the done callback within the target function. No call to done test fails.

Next, we call debounce . As we chose earlier, we pass in the two parameters and grab the output. Lastly, we call the output to test this behavior: After calling the delayed function and a delay, the target function should get called.

We don’t need to know the exact implementation for this we just plugged in the information from our previous steps directly into a test. The only thing we need to know is that in javascript, delays are asynchronous, so we need an asynchronous test. Yes, this is perhaps an implementation detail, but it follows naturally when you know how JavaScript works and is in no way specific to this particular function.

We can go ahead to step 5 and implement the code now:

function debounce(targetFn, delay) { return function() { targetFn(); }; }

Wait a minute! That isn’t delaying the function at all!

Yes we’re doing TDD! Arguably all we need to do is satisfy the test we wrote… and this code makes the test pass.

One might call this a bit cheaty, after all we know this isn’t the correct behavior. One interpretation of TDD only calls for implementing just enough code to make the test pass, so let’s play along.

We’ll go back into step 3 and choose another tiny behavior to implement. A very important behavior would be that the function doesn’t get called too early, like our code does right now.

OK, that’s our second tiny step forwards. Step 4, implement test:

it('should not run debounced function too early', function() { var delay = 100; var targetFn = function() { }; var delayedFn = debounce(targetFn, delay); delayedFn(); //but how do we verify it now? });

We’re going to need Sinon’s fake timers. We can use them to create a fake timer and then advance it forwards, and then ensure the delayed function isn’t called earlier than it’s supposed to.

it('should not run debounced function too early', function() { var clock = sinon.useFakeTimers(); var delay = 100; var targetFn = sinon.spy(); var delayedFn = debounce(targetFn, delay); delayedFn(); clock.tick(delay - 1); clock.restore(); sinon.assert.notCalled(targetFn); });

First, we enable Sinon’s fake timers. Notice I changed the target function into a Sinon spy. This allows us to later easily verify if the function was called or not.

After we call delayedFn , we use clock.tick to advance the time. However, we only advance it 1 millisecond less than is required for delay. This way, as we call sinon.assert.notCalled , we can ensure the target function didn’t get triggered too early.

If you want to learn more about Sinon’s functionality or fake timers, you should grab my free Sinon.js in the Real-World guide , as it covers this in much more detail.

Conclusion

As you can see from the examples, we can apply the same five steps to all sorts of functions.

If you’re looking for some practice, you could take either of the two functions we started implementing here, and seeing if you can apply the 5 steps to make those functions fully functional.

Test-Driven Development is not difficult once you get the hang of the basics. The challenge is that it requires you to flip your thinking around: Without TDD, you think directly of how you implement something. But with TDD, you think of how you want something to behave.

What are the inputs to our function and what is the output (behavior) we want from calling the function? Decide how calling the function from code works Choose the smallest possible piece of behavior for some inputs that you can think of Write a test which uses those inputs to call the function, and verify the behavior Implement enough code to make the test pass

If we follow these kinds of simple steps, writing tests up front becomes much easier. As you continue working on the code, you can just repeat between step 3 to 5.

Remember if you implement some tests and code only to later find out it has to work differently, that’s fine! Go ahead and redo it We don’t need perfection on the first try, seeking it only gets you stuck. This isn’t just a TDD thing either: you’ll probably need to redo and refactor parts of your code anyway, TDD simply makes it safer because you have tests that verify your code doesn’t break as a result of changing it.

If you want to learn more practical tips on things that make the life of a professional JavaScript developer easier (including stuff on testing), you should sign up for my newsletter with the form below.

本文前端(javascript)相关术语:javascript是什么意思 javascript下载 javascript权威指南 javascript基础教程 javascript 正则表达式 javascript设计模式 javascript高级程序设计 精通javascript javascript教程

主题: JavaScriptJava
分页:12
转载请注明
本文标题:5 step method to make test-driven development and unit testing easy (examples in ...
本站链接:http://www.codesec.net/view/480882.html
分享请点击:


1.凡CodeSecTeam转载的文章,均出自其它媒体或其他官网介绍,目的在于传递更多的信息,并不代表本站赞同其观点和其真实性负责;
2.转载的文章仅代表原创作者观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,本站对该文以及其中全部或者部分内容、文字的真实性、完整性、及时性,不作出任何保证或承若;
3.如本站转载稿涉及版权等问题,请作者及时联系本站,我们会及时处理。
登录后可拥有收藏文章、关注作者等权限...
技术大类 技术大类 | 前端(javascript) | 评论(0) | 阅读(44)