 OK, TDD, test-driven development. So how many people have heard of it before? Right. So in my day-to-day work, in ThoughtWorks, we use TDD every day. With my experience, I think it's a very good engineering practice for coding and development. So yeah, I like to share something about test-driven development. So before that, let's talk about testing. Why testing? Well, a lot of reasons listed here. So by testing, we can verify functionality of your application. You know what your app is doing, and the catch bugs, and prevent them from coming back. Also serves as a documentation for your maintenance process. For example, if you have tests written well, then, for example, later, you left the project, or you are not working on this piece of code anymore. Other people can know what the functionality is based on the testing. So testing is a very, very big, very general concept. How do we make sure? There are a few points that people may concern about. So firstly is the code coverage, and how people are confident about all the scenarios are covered, and the prevent bugs. How does testing do it? And the answer is TDD. So this is where TDD comes into the picture. Let me talk about more details about it. So firstly, what is TDD? Test-driven development. So there are three status. I don't know whether you have saw the picture that I used for today's workshop. It's a red-green refactor. So three faces, red, which is the first one. Writing tests before writing your code. So you write the test, then it fills. That's the red part. And after it fills, you write the minimum amount of code necessary to make the tests that you write to pass. That's the green face. Then continuously, you refactor your code, make sure that your existing code pass all the test cases. And why we should do it? So back to the previous post questions that we had just now. The three things, code coverage, scenario, and the cases covered, and the bug prevention. Let's look at the first question first, the code coverage. So before writing the code, before the implementation, you have the test cases. This ensures all the code covered by your test. For example, you don't have any code written for your functionality yet. And you write test cases for the code. And then you implement. You will know exactly this piece of implementation is making the test pass. So this is ensuring all the written code are tested, like even before they're written. So usually, if TDD, without consciously checking the test coverage, it's usually have a very high test coverage. And the second question, scenario covered. So how TDD works is usually you think about what your application, what your program is doing. And you have all the scenarios. Because you have to write it in the test first, so you know exactly what it's doing. So it's visible these two functionalities and easy to see which cases are missing. In a way, that unit test make you force you to think about all the edge cases of the program. And the last question, bug prevention. So when you are writing a test-driven development, when you are coming up with all those unit tests, you are probably thinking about what are the edge cases, like the non-check or undefined value check. Then when you are implementing, you are making all those edge cases pass. And also, for my personal experience, I like TDD one of the most important reasons because we continuously refactor our code to make it in a better structure. And with all the test cases, I'm very confident to refactor the existing code. I can rewrite a piece of code without breaking the functionality. That's very important. So how should the code be used? And only a read-necessary code. So this helps with your design, like what I said before. Before your implementation, you know what exactly you are going to write for your program. Who knows what's the full version of Yegni? So basically, you just have the minimum amount of code. You don't have extra code like nobody cares about lying in your source code somewhere. And it will become a problem later in the project. So this makes the code very clean and very lean. It's only stick to your test cases. So how do we do that? Write a green refactor. Write is when you write a test case before implementation, it's going to fail because, obviously, you don't have implementation. And then you implement for the test case that you write to make sure this piece of code is tested and it's doing what it's doing. And then continuously refactor the code and make better quality of the code. OK, now let's go to the workshop part. Let's get our hands dirty. I have a repository for everyone to clone. We can start from there. This is the link. Let me just paste it here. This is one branch of it. There are two branches on this repo. One is the master branch is with full solution and this is the bare bone code to start with. Can clone this by doing OK. Yeah, I have something here. Git clone. Yeah, you can specify the branch practice. Git clone dash b practice with the link. Has everyone cloned the repo? OK, so after cloning the practice branch, the projects look something like this. It has one mean file which calls here, streammumify.js and a test file in the test folder called streammumify spec. And before that, let me show you what we are going to do for today's practice. So we're going to write a function, very simple. We're just going to write one function to mummify stream. Maybe you have heard of this problem already. OK, back there, everyone got the repo. Yeah, good. All right. Oh, the Wi-Fi password is, so there is a QR code there. You can scan that. Or the red text on it is the password. It's five words, it's pretty long. Sorry about that. Let me just get the password here. The Wi-Fi network name is twguest. And the password is crimson, space, t-r-o-u-t, space, rewrite, space, trustable, space, i-v-y. Yeah, this is the password with space in between. Yeah, let me know if you have any problem being in the middle of the workshop. All right, so back to the problem. So the input of this function is a string. And the string is of different lengths. But it's a combination of vowels and other characters. If the vowels are more than 30% of the string length, then you insert mummy for each continuous set of vowels. Here is some example. So here's one third, which is around 33% of the length of the string. Then you replace the vowel with mummy. This word is mummy-fed as h-mummy-es. And the second example is here. So if you have continuous vowels in the middle of the word, for example, in this case, it's 50% for the length. You replace the continuous vowels with one mummy, not two, not the, yeah, as demonstrated in the third one. So the input is a string. The output is a mummy-fed string. So yeah, the function is as simple as that. We have a, so can anyone think about, you've given this requirement, which will be the first unit test that you think you are going to write? Yeah, anything, just you can just speak out. It's fine. OK, yeah, so I have several scenarios here. The important point of practicing TDD is the test case must be unit, so it covers the features, like the atomic features of your application. So when you try to debug or you try to use the TDD code as a documentation, you will know which part is it. And it's like, because the unit test is going to cover just one single functionality, so it's easy to debug too. So the first test case will be, if I gave it an empty string, I'm expecting this function to return me an empty string. All right, and the second, OK, let's start with the first one. Let's switch back to the DEMI repo. So it's a Node project. If you have Node and NPM installed, you can just do NPM install in the project folder. Then the relevant packages, specifically in package.json, will be installed. So you don't need to worry about the testing framework. Today's testing framework I'm going to use is chai and here. So everything is specified here. Yeah, I'm going to use mocha and chai. Mocha is the test runner, and chai is the searching framework. But all this, you don't need to worry about. Today is more about the concept of TDD rather than the tool that you're going to use. The concept can be used on all different programming languages, even Java, C++. And I'm just using Node and the JavaScript as an example. So if you do this, I should install the relevant packages. All right, and the syntax for mocha is quite straightforward. So for example, here, I have a describe. Describe is basically just to group a list of unit tests together in case that they share some common features. For example, I'm going to just test the function mummify in this file. So I'm going to have a describe to include all the mummify related test cases here. And the eight is the keyword to start with the unit test, like which test exactly you are going to write. And the first test case, as I demonstrated just now, I'm giving it an empty string. I'm expecting it to return me an empty string. So if you, in the terminal, if you type yarn test, it should start the mocha runner to run the test. It's all configured in package of JSON, ever already configured. So you just need to run this. It should be all right. Done, done, done. Dive tdd row drop. OK. So for now, because we don't have any expectation, we don't have any assertion yet, it's just giving me, if you look at the console, just saying mummify, then something passed. Because we don't have expectations, so everything is going to pass now. And I'm going to write my first test case, which is expecting the string to be an empty string. The syntax for chai is expect. And I'm imported mummify. I'm giving it an empty string. JavaScript, you can give single code or double code, doesn't really matter. To equal to b. And this is the right face for the first test case. So it's saying that mummify should not mummify empty string. And the assertion error is saying that expecting undefined to equal empty string. And then after that, we go to the green face, which is the implementation. I'm not going to show the answer first. You can implement yourself. In this function, defined in string mummify.js, just to make the minimum change to make that test pass. And also, if you really want to refer to the solution, you can just give the checkout master, it will be out there. So I mentioned something in the presentation, saying that your test can be a documentation. As for example, this case, it's very clear that your mummify should or should not do something. So yeah, this is the example of how your test cases serve this documentation. If anyone want to share the answer, it's really simple. It's just one line change. OK. Sorry? Yeah. OK, pass. OK. And how? Oh, I just see that. Right. You want to share? You want to share this with me? OK. Yeah. And sorry, your name is? Kaiyog. Kaiyog. OK. You want to just use my laptop just to show this? Come. Yeah. Yeah, you can go with the next one, too. All right. So Kaiyog here is going to give the answer to the first test case. Yeah. Yeah, so this is what I'm saying. Make the minimum change to make the test case passed. And here, let me run again. You're not. Well, ah, wrong. Sorry. The one is like, I'm not sure why my console is not giving color. Yeah, the text is gone. Yeah, it's not showing. OK. Right. So this is a green face. The minimum code to make the test case passed is only to return an empty string. All right. And then we continue with the next test case. To give it a RTR, return a RTR, because there's no bubble in the middle of it, so it's not going to be mummified. And who wants to try to write the test case? I can start the bubble here. Should not mummify. Wow. Leo. Yeah, thanks. OK. Yeah, so who wants to implement the test case? Hands up. I don't want to try. Just come here and demonstrate what you want to write. Or I can do it. I don't mind to do it. It's too easy, right? So this will be the test case. If you run it, yeah, it's failing. So you're expecting a string, which is an str, but it's returning the empty string because your implementation here is to return an empty string. And now you have to make the implementation and make sure two of the test cases, both of them are passing. I put the test case here, and you can do the implementation. It's going to share her solution about the second test case, and you want to, OK, I do it. Yeah, yeah, I do it, I do it. So yeah, so in the implementation, she just changed the empty string to the word. So what's in, what's out? And if we run the test again, yeah, both are passing. Of course, this is not the final solution that we are going to have for the MamiFi function. So in this manner, we write the test case first and then write the implementation or refactor your current implementation is to make sure that all the test cases are passing. OK, I'm going to put all the scenarios for this workshop here, and you can start to write the test cases and implement by yourself and by one from here. Yeah, and do let me know if there is any problem with the syntax or no jazz package problem or git problem and I think travel. Yeah, so the third test cases will be a single vowel. Just to clarify a little bit about the structure here. So this string MamiFi is actually the file for your implementation for the function. And this string MamiFi spec, which is a convention in some JavaScript framework to say that this is the specifications for the implementation, meaning this is the test case files for the implementation. And there is something handy that you can do for the test case is in the MoCA framework. For example, you have two test cases and your implementation may be something like that. And it's filling, right? And there are two test cases here. The first one is passing, the second one is filling. And you want to get rid of some of the noise. You can just focus on the filling one first. So you can do eight dot only. It will run only the second test case. So if you see here, it's saying zero passing one filling and the filling one is the one that you want to focus on. And this is to include some of the test cases. And there is another keyword called x. You put x before eight. It means exclude this test case. So if you do this, when you do an NPM test, you only run the first one. The second one is excluded and is ignored. So no matter what you are expecting, the second test is doing is not going to give you anything useful. Yeah. This is helpful when you have like 100 of test cases and you don't want to run everything again and again. You just focus on one or two test cases that you want to make it pass. All right. So who wants to give the solution for the third test cases? For the third test case. Sorry. No, I didn't. No, I didn't. OK. All right. I'll try this. OK. So for the third one, my test case will be firstly have the description about what the test case is going to do. So the third one is to change a single vowel to mummify. So should mummify if passing a single vowel letter. And I'm expecting that if I give it a single vowel, it shouldn't return me a mummify. Mummify, right? M-mummy. And I'm going to run the test case again. OK. My first two test cases are passing because I haven't changed anything about the previous implementation. And the third one is failing. It's expecting mummify, but it returns a because the implementation saying that whichever you pass in, I will just return it. So yeah. Who want to share the implementation part? So if you're thinking about checking whether a letter is like A-E-I-O-U in JavaScript, there's an easy way to do that rather than regex. So for example, I'm just using a console to show this. I have a variable called vowels, which is the same as what's in the code, like saying it's a string A-E-I-O-U. And the index of is returning the index of certain letters in this case, in the string. So if you want to check whether a char is in a string, you can just checking whether the return value of index of is greater or equal to 0. So if it's not minus 1, then it's part of it. If I'm typing Y, it's written me minus 1, which is a invalid index. So you can use this for the implementation. Just in case you're not familiar with the JavaScript syntax. OK, so for the third one, if the passing argument is a single wall, we want to mummify it, right? And we don't want to break the previous two test cases. This is the implementation before. It's filling on the third one, passing on the previous two. The minimum code that I'm going to have to make all three test cases pass. Well, we can start with the third one first. So thinking, make the third one pass. We don't really care about the previous two for now. If I put it only here, but usually you do care about all the test cases. So this is just a demonstration. If wall.index of word, then return mommy. And so my thinking process will be, OK, I'm giving an A and I want a mommy. So I'm being lazy. I'm just returning a mommy without any other conditional checking. And here, right, sorry, index of is great. So my third test case is passing. Now I need to make sure that it's not breaking other test cases. I remove the only and run everything. And something is filling. The first one is filling. So it's saying, OK, your implementation is not good enough. It's not satisfying all the scenarios it should cover. And the reason is because the first test case, I'm passing an empty string. So here, I'm just going to give another check. If it's empty string, then return empty string. I'm going back to run all my test cases again. All three passes. Yay. Yeah, but it's not very clean code because you're basically just checking for each test cases, right? But now, because you're confident that your test cases are covering all these three scenarios, so this is a time you can think about to do some refactoring. Red, green, refactor. Yeah, this can be taken as a point for refactoring. So either you refactor at this point, because for now, all three test cases passing. And you have all these test cases here that you need to cover. You can do more complicated implementation than later thinking about what you're going to refactor, how you want to organize your functions. So yeah, anyone else make the third test case pass? You can share your solution. The implementation could be very different for everyone. Anyone? The previous test case, this is one implementation to make the test case pass. Another one, like the lazy one, is just to say that if the word equals to A, return mommy. But then it will feel like some other test cases. Like for example, if you do really want to check each vowel letter, so you will have five test cases to check A, E, I, O, U separately. And yeah, basically, it will only pass A, and the E, I, O, U will fail. So yeah, I'm just taking the next step, just checking whether it's in the string. Test, for example, this one. And here I'm saying expect mommyfy A, which is calling the function. It's exactly the same that if you have a variable called mommyfied. So it's the same as you call the function, get the return value, and expect the value to equal some string. And if you want to debug this function, for example, it's just not working the way that you wanted to work. You can console log, and it will show there. Yeah, this is something that you can use for debugging. Yeah, and I'm still at the state that I have three test cases. And I was just doing a little bit refactoring. So just now I was checking whether the word is empty string, and whether it's a vowel. And now I'm saying that if the word is defined, and it's not known, and it's not empty string, then check whether it's a vowel. And if it is, return mommy. Otherwise, just return whichever you pass to it. And after the refactoring, I'm running the test case and everything passing. It means your refactor code works. So you don't have to worry about whether I change this piece of implementation, whether it will break anything. So this is a very nice thing about TDD that I make sure that what you want is always in the implementation, no matter when and what you do for the implementation. All right. Yeah, so any questions about this? If not, I am going to show you the full implementation of the function and how it looks like in the tests. So this is some solution I did before the workshop. So basically, it has a very clear structure that what is expecting, what is not expecting. And when you run it, it's very nicely documented what your function is doing. And for example, you're not working on the project or you're not working on this feature anymore for the maintenance or for someone to pick up what you have left. It's very clear to them what exists. What existed? Yeah, so well, if you check out the master branch, you have the implementation and also the tests. So if you haven't finished the whole thing, which is quite normal, you can do it later when you go back home. And let me know if you have any questions about this. Any question about this so far? Sorry? And what detail we should write the end test? In which details. So how you come up with the test cases, right? It depends on what problem they're trying to solve. It should be as unit as possible. So one test case is only taking care of one scenario. Later, if something breaks, you know exactly what is breaking. For example, in this practice, I have these scenarios covered. Or alternatively, if I have only, for example, one test case saying that mummify should do something, should mummify the string, it's very abstract and it sounds very complicated, right? It doesn't give the expectation. It doesn't say that what input should be given and what I'm expecting to be the output. So the test case should cover the functionalities that your function is expected to behave, as well as the edge cases that should be covered. For example, in this one, the last one, if an undefined input is given, you should think about what the behavior should be. And in this case, I'm expecting it to zero error. And you can do some other error handling to change the expectations. So yeah, that's another thing about TDD that if also to think about the edge cases and your test should cover the edge cases. So the last one, yeah. So if I'm not giving any argument to mummify function, it should zero error. And the implementation. Well, it's not explicitly stated here. But it's, let's see. Yeah, it's not explicitly stated in the code, but here it will fail because word is an undefined value and it will throw some error by the framework itself. Or you can handle it to catch some specific error type that you customize yourself. That will be better with a specified error message. Yeah. So a short version of the answer is as unit as possible and covers the positive path and the negative path and edge cases. Yeah. All right. I think that'll be it for today's workshop. Anything else? Yay, thank you. Thank you everyone for coming here and joining me. Yeah. And if you have any questions about TDD or you are interested in ThoughtWorks or you are interested in women who code, you can just come to me or drop me an email or just send me a message on Meetup or something. Yeah. Thank you.