 Okay, so hi everyone, my name is Jan McGehrke and I would like to give you some insights into how we try to ensure that our apps work as intended by testing the code of our apps. Before we start, I would like to mention that the exercises, which I will introduce later, can only be worked on with Chrome, Chromium or Brave. Unfortunately Firefox has some issues with the code sandbox's testing functionality. You're going to have some time after my presentation before working on the exercises to install another browser so you do not need to do that during the presentation. I have divided this presentation into two parts. In the first part we will talk about what testing is, why it is important and what different types of testing exist. There's a small part about end-to-end tests with our own tools, but it won't be the focus of this presentation. And in the second part we'll look into the specifics of how to write unit tests, including some live coding. Okay, so every time we write code it's normal that we don't spot all the typos or edge cases that will cause bugs occurring on the end user side. And especially when changing existing code that the developer hasn't worked with before, it's near to impossible to know every possible scenario that the code has to handle. So by testing code you're reducing bugs. And in order to make refactoring easier and to prevent user-facing bugs, the code can be tested with testing tools to make sure the erroneous parts are reduced to a minimum. And by doing this a lot of time can be saved with tests. This can be done by testing bits and pieces of the code or testing the entire stack. When testing the smallest part of the code for example functions and classes, the test makes sure that for a given input or state the function or class will return an expected value or the components render expected html. Testing entire apps will ensure that the app works in the intended way while not testing any code directly. Tools that help testing apps open a browser and click or type just the user would do. Of course apps and code could be tested manually but that's a cumbersome and time-consuming task. Without a dedicated team, manual testing is quite unreliable. That's why tools can be used to automate these tasks, adding robustness to the app while reducing the burden on the quality assurance team. Writing tests does take more time than simply writing code, which is why it's often seen as a hurdle. But it offers several benefits that make adding tests worthwhile. By covering app and code with tests, the amount of user-facing bugs and the time to confirm and fix bugs is reduced. Additionally the time it takes to get back into the development flow is reduced as developers don't have to handle unexpected issues. This is especially true for apps that are going to be used by djs2 instances as what's normally being handled is critical health-related information. Testing apps can make a difference in data collection and display, so covering apps by both core developers as well as third-party developers is very important and yet an often overlooked aspect. At djs2 this has been a topic that we have not addressed properly for way too long, but roughly two years ago we started adding tests to our apps and libraries and we all see and appreciate all the benefits this adds to our daily routine. It feels a lot safer to work on apps as we know that all the functionality that we covered with tests stays intact. Additionally to just covering code with tests after the code has been written, there are several alternative approaches. One of these is called test-driven development or just TDD. This means that tests are being written before the actual code, which means that the benefit that the developer has to think about the final structure and logic before writing code, that she or he has to change eventually again when making up the structure on the go. As mentioned there are different levels of an app that can be tested. One of these is so-called is called end-to-end tests. We go through what end-to-end tests are and what tools we use at djs2 to implement these tests. End-to-end tests don't test actual code. They instead try to test the entire product from beginning to end to ensure that the application flow behaves as expected. In other words, an application is tested from one end to the other, which of course includes the user's perspective. The tool that we use at djs2 is called Cypress. We will not cover how to use Cypress during this workshop, but I want to give a quick overview over the software so you can take a look yourself if you're interested. Cypress is a command line tool with a graphical user interface for development. It opens the app inside the browser and clicks on types on elements that a user would interact with as well. While interacting with the app, the software can perform checks to test whether an interaction has caused the app to perform desired behavior. With the graphical interface, every step of the test can be expected. This includes the DOM in the developer tools as well. This is quite handy when developing tests as it's easy to see why a test is failing. The graphical user interface will show all available tests, as well as a drop-down in the upper right corner, which you can use to choose a different browser. On the image, you can see some of the tests that we use to test our UI library components, like the alert bar or button. On this screenshot, you can see the tests for the transport component being executed. We're testing one specific feature here, which is allowing the user to highlight options with key combinations, like clicking with control, command or shift. In the center, you can see the actual component that's being tested. On the right-hand side, there are developer tools, which can be used to inspect the current state of the HML at any state of the test. For this case, I'm inspecting one particular diff and see if the CSS is applied properly. This allows to write tests relatively quickly and comfortably. If one of the tests fails, Cyprus will provide details about what went wrong. It will let you know about the origin of your error, which might come from the application or the test code itself. In case an error occurs, you can see the console output and inspect the DOM at the time the error occurred. This is what makes Cyprus quite a powerful tool. This is one of many reasons why we will increase the test coverage of our apps with Cyprus and our own CLI utilities that extend the functionality of Cyprus. The test that you just saw in the previous screenshot is described in this file, a so-called feature file, which is divided into scenarios. As you can see, this is more or less normal English, which is another advantage of using these types of tests, as even non-developers are able to read what the software should be capable of. If this is interesting to you, I have included a couple of links in a readme that I will share with you later. Knowing how Cyprus works is quite crucial when working with our CLI utilities. Internally, we're using our own utility. It's relatively new, so most of our apps do not use it yet, but we're planning on adding it to more and more apps over time. We're actively working on the tool as well. In the near future, we will be releasing some new utilities to make Cyprus even record and replay network requests, so the tool can be used in conjunction with the continuous integration tools without having to set up a new leadge as to instance and database for every individual test. In this readme that I'm going to share with you after the presentation, I've included some useful links, including the links to our Cyprus utility. If you want to get into Cyprus, you can find the documentation here. It will tell you how to install the tool. It will tell you how to set up Cyprus for your project, how to enable automatic login with this tool, how to add the login credentials, and some other useful information. If you want to have an example of how we're using this at DJI's tool, there's not a link to the code of the SMS configuration app. So this is the SMS configuration app, and here you can see we have a Cyprus folder and here in the integration folder, we have different feature files. For example, for adding an SMS command of type alert. So you can see here that we have different scenarios. Here's the testing code, and if you want to try this out yourself with this app, you can just clone the code, start a new instance with our D2 cluster utility. You will have to install the dependencies, of course, with Jan. You will also have to add the Cyprus.nflotjson with your login credentials and the URL of the instance, which would be localhost with port 8080 if you use the D2 cluster command, and then you just need to run Jan, Cy, colon run, and then it will execute the test and you can play around with this yourself. Another way of testing code is to test the smallest parts of the code. This is what we will have a look at today and what the exercises later will be about. But if you have attended the workshop last year in August or if you're already familiar with Jest, you could alternatively also try to play around with the Cyprus tool or read through the documentation. Okay, so what are unit tests? As mentioned in the introduction, with unit tests, we can test the smallest units of the code like functions or classes and their methods. These are very lightweight tests, so it takes a fairly low amount of time to run these. This allows us to integrate these tests into our daily workflow like in the pre-commit hook, which ensures that code that doesn't pass tests can't be added to a code base. This is especially useful when changing existing code. If the change code will produce unwanted behavior in a part of our code that hasn't been touched, this might be caught with these tests before it ever makes it into production. Michael Lynch says in his blog post about why good developers write bad tests, that a good unit test is often small enough that a developer can conceptualize all the logic at once. A very common tool to write unit tests with is called Jest. It's developed and maintained by Facebook's developers and contains a complete package to write unit tests for JavaScript projects. It's widely used, so for functionality that's not covered by Jest itself, like testing React components, there are solutions that cover the missing parts like Enzyme, which is a React component testing library developed by Airbnb. Jest allows you to run tests in your terminal, which is way faster than using Cypress. In the screenshot, you can see that 164 tests were executed in just four seconds. When a test fails, Jest will tell you what exactly went wrong as well. In this case, the test expected only valid emails to be passed to the email validator function. The description associated with the test states, should return undefined for value, I am not a valid email address of type string. Obviously, I am not a valid email address, is not a valid email address, so this test fails for a good reason. Jest also caches its results, so if a test and all its related files have not changed, it treats the test as having passed, which is why it only took a bit more than six seconds to run 486 tests on my computer. When you are working on a platform app, then you don't have to add or configure Jest. It is included in the app platform scripts. Once you have added tests, you can simply run the yarn test. This will run all your tests and print the result on the console. If you don't change Jest's default configuration, you will have to add the .test.js or .spec.js file extension to your files. Now we are going to do some coding together. I have prepared some code. In this case, a function and a component that we are going to test. Here you can see a function that is called getLabelByType. It expects an object, which is then called instance. It expects this instance to have a property called type, and if the instance does not have the type, it is going to throw an error. If the type is top, it is going to return premium tier. If the type is mid, it is going to return top tier. Here you can see that the developers chose a different terminology than stakeholders or whoever. It is good to actually test this. It is working correctly. If the type is low, it is going to return value tier. If it is none of these three, it is just going to return the type itself. We want to make sure that this works as expected. There are different ways how to use just. One would be to use the test function provided by just. Then we can just say one plus two equals three. In this function that I am going to provide to this test function, I can say expect one plus two to be three. Let's see if this works. I can run the test by running the yarn test, which executes just the background. Actually, let me just run this file specifically. I can do that by supplying the path to the file. Now it is just going to run the test for this file. You can see one plus two equals three. I can make this test for any purpose and see what happens. I can also add the watch argument and then this is going to rerun the test every time this file changes or one of the files is going to use by this one. Here you can see that we expected value four, but it actually received three. Of course, this is wrong. I can fix the test again by just changing this to three, saving it. Then this test is going to work. You can see that the other tests in here, which don't have any test code at all, are passing, which is because there are no expectations inside these functions. I am not really a big fan of using the test function. I prefer to use describe because with this one we can choose a unit in our test like this function and then handle different cases that this unit has to handle. I am saying this. I am just adding describe then a string, which is what you can see here. Then inside this function that I am providing as well, I can use the it function. It basically stands for whatever is in the string here. Instead of reading it should return premium tier, I could also say get labeled by type should return premium tier when type is top. I am going to test that now. The expected value is top. The actual value is get labeled by type. I need to pass in an object with a type property that is expected as premium tier and type is top. Then again before I can just say expect actual to be expected. Of course I could just provide this directly. It would work as well, but this makes it quite readable. If I run this one by just saving it, you can see the test passes and if I change the contents of the type, it should not pass anymore. You can see it fails and says expected premium tier but received full. It changes back and it should pass again. Oops. Now we can do the same with top tier and this should be returned when we use type mid. Let's see if this works. It does. Let's see if I can break it if I use the wrong value here. As you can see, so the test actually makes sure that this is working. I can do exactly the same with low as well. It should be valued here and I can do the same with the custom type. If type is none of the three above, something like this, it should return to type itself. Now this should fail because I'm still using your one here, but you can see it actually returned custom type, so this is correct. Now the test passes. We successfully covered this function. Now if another developer changes this for whatever reason, like this for example, then the tests are going to tell him that he broke the tests. Either he has to update the test because this is a valid change or he has to revert his changes to make the test pass again. Here I've prepared another test for testing a React component. To test React components, you have to, if you're using Enzyme, which is the library by Airbnb that I've mentioned earlier, you have to configure this first with the app platform, you don't have to think about this. You don't really have to worry about this. Now the important part that we're going to use is mount. So here I have a component data element that expects a loading error and data prop and if loading is truly, then it's going to render some HTML with the content loading data element. If the error is truly, then we're going to render the error message itself. Otherwise, we're going to render the ID and the display name. So to make sure that this works correctly, I can use the mount function that I just imported from Enzyme. I can render this component as I would normally would do with React as well. And then loading equals true. So here we're going to test that it's going to render the loading text. And then I can, so you can see here that the, this component, this element containing the text has a class loading. So I can try to find that loading element by using the mounted component find and then load. This is going to return the element that's inside this component with the class loading. And then I can do an assertion as earlier. I can extract the text that's inside this element by using the text method and then say, this should be, let's see what exactly it should be, loading data element. So I can do this. Now, another thing that I can do with Jest is I can tell Jest to only run this test and ignore all the others by using the only. So in here, it's still running the old tests. I'm just going to change it to data element. I was going to run the test for this test file and you can see that it skipped two tests because I told it to run only this one. Alternatively, I could tell these two to skip, which is going to produce the same output here. So it's only running this first test here. Let's see if I can break this by changing something here. And you can see it says I'm expecting or the test is expecting loading data element two, which is wrong. It received loading data element. So we can fix this test again by removing that part. Now it should pass again. We can do the same thing for the error. So I'm going to give this data element an error now and this is not going to work. This is going to fail because loading is still set to true. I still have to, of course, I still have to error. What should it display? Error loading data element and then the error message. So it should pass the error. This is what it should display. This is going to fail because loading is still true and in the function you can see that loading is handled before error and it returns. So if this is true, the function will never execute this part of the code. So first I have to either set loading to false and you can see it works now or I can entirely remove this one because then if I don't provide the properties undefined, which is also falsey, and it's working again. Now I can do the same thing for the testing when the data is provided. If this works, it expects a data element with an ID and the display name and expects the data to be this data. And then we can again try to find the ID and display name inside the component. ID element is component.find.id, display name element is this component and then we can say expect the ID element text to be data element i. And then display name element should display this is my display name. So let's see what happens. This is going to fail because you can see that it says cannot read property ID of undefined. So we just provided data element with an ID property, but data element actually has to be inside of data. So I have to wrap this data element inside this data element with an object and provide data to this data element component and it still did not work. Of course this has to be display name. And you can see now that it has passes. I can remove all the skips. So now it should run all tests and you can see all of them pass and because we implemented the tests of both files we can run all tests and now this should pass all test should pass. You can see this one passed this one passed it executed seven tests into files in 4.5 seconds and most of this time is because just need some initialization to do and then running the tests itself is quite fast. So I'm going to share with you the exercises in a second. In here you will find the link to the code sandbox exercises and in the source folder there are eight files, four for regular JavaScript unit tests and four files for component tests and each of these exercises increases in difficulty. So the first one should be fairly straightforward. This is the function that we just took a look at. There is a comment that describes what you have to do. You can see the implementation is missing here. Same for the component tests. You just everything is important already that you need so you just have to go down to the tests and read the comments and implement what you what is expected here and you can see here instead of browser there's also tests and it will tell you whether the tests pass or not. The first time I load this normally just a test phase but none of these tests have any assertions so they're all going to pass. There's also the solutions but I highly encourage you to not look at these solutions and try it out yourself. If you're struggling before going to the solutions you should try to read the documentation because this is what you will spend most of your time with when writing tests until you get used to using these libraries and I think this is one of the best skills to have when writing tests is to know your way around the documentation. Are there any questions? There was a question like Pete and I think you can unmute yourself. Right Martin? I'm going to get the question is in the chat on Slack in the channel. On Slack give me a second. Is it best practice to provide a test string value multiple times in a test or reference the original variable again for example this is my discipline rather than data element dot discipline and that's a good question. Let me share my screen again. I prefer to just write this string again. In this case it doesn't really matter. You could just reference it again here but by writing out the string again it's easier to read the actual test code which could serve as documentation as well. For more complex tests or more complicated tests it's actually better to write out stuff again instead of making things in quotes dynamic or a little bit more abstract because tests should be as simple as possible to make sure that everything works as intended. You're not trying to make right elegant tests or make this really super abstract and reuse as much code as possible. These are meant to test stuff so my preference is to just write this out again and have lots of duplication in here which is normally not good in application code but in tests it's actually encouraged so yeah I prefer to put this one in here again for several reasons.