diff --git a/common-content/en/module/git-cli/adding-committing/index.md b/common-content/en/module/git-cli/adding-committing/index.md new file mode 100644 index 000000000..b98bf5466 --- /dev/null +++ b/common-content/en/module/git-cli/adding-committing/index.md @@ -0,0 +1,131 @@ ++++ +title = 'Adding & Committing' + +time = 30 +[objectives] + 1='Stage a change using the terminal' + 2='Make a commit using the terminal' + 3='Check the status of a repository' + 4='Access the commit history' +[build] + render = 'never' + list = 'local' + publishResources = false + ++++ + +It's time to create a file to work in. Use the terminal to create `notes.txt` then open the directory in VSCode. + +```sh {title="git-cli-practice"} +touch notes.txt +``` + +Just like last time we'll add some text to the file and save it. + +```text {title="notes.txt"} +Git in the Terminal + +I'm learning how to use Git from the command line. +It's going well so far! +``` + +### Staging changes + +Let's move back to the terminal. If your terminal may have some sort of indication that something has changed, but it may not. We can always use `git status` again to check the state of our repository. This time the output will look like this: + +```console {title="git-cli-practice"} +On branch main + +No commits yet + +Untracked files: + (use "git add ..." to include in what will be committed) + notes.txt + +nothing added to commit but untracked files present (use "git add" to track) +``` + +This is the terminal equivalent of the big green "U" next to the file name in VSCode. Git is very helpfully telling us the command we need to stage our change so let's go ahead and do it. + +```sh {title="git-cli-practice"} +git add notes.txt +``` + +Now if we check our repository's status again we see a different message: + +```console {title="git-cli-practice"} +On branch main + +No commits yet + +Changes to be committed: + (use "git rm --cached ..." to unstage) + new file: notes.txt +``` + +We can add multiple files at once if we want to by passing multiple arguments to `git add`: + +```sh +git add file1.txt file2.txt #... +``` + +{{}} +Adding individual files gives us precise control over what we want to stage but it can be a little cumbersome if we have lots of files to stage at once. There are other ways of staging changes which capture multiple files at once: + +- `git add .` - This stages all changes in the **current directory**. +- `git add --all` - This stages all changes in the **current repository**. + +At the moment our repository is a single directory so these commands will do the same thing, but as our projects get more complex the distinction becomes useful. +{{}} + +### Making a commit + +The next step in the process is to commit our staged change. We still need to provide a commit message and it's still very important that it tells our colleagues **what** we changed and **why**. We don't have a text box to type it in though, so how do we supply the information? + +We will use the `git commit` command to make the commit but we will use the `-m` flag to provide the commit message. + +```sh {title="git-cli-practice"} +git commit -m "adding initial notes" +``` + +Our commit has now been made. + +{{}} +Forgetting to add the `-m` flag is another common error when learning Git. A commit _must_ have a message associated with it, so if you forget to include one you will be prompted to add one before the commit is made. Git will open your default text editor and prompt you to add the message. When you are done you can save and close the file and you will be in the same place you would have been using the flag. +{{}} + +Another `git status` check will have us back at the "nothing to commit" stage. + +### Viewing the commit history + +We no longer have our repository's history represented as a nicely-coloured timeline, but we do still have access to the information. The `git log` command will show us the same information, and will actually give us even more! + +```console +commit dc976c03d859a368281ff8875997d1afa6e643d8 (HEAD -> main) +Author: A. User +Date: Thu Sep 17 17:10:24 2026 +0100 + + adding initial notes +``` + +We can see all the same information that VSCode provided us with: + +- The commit message +- The user who made the commit +- The date and time of the commit +- The branch the commit was made to + +We also have a long hexadecimal number on the first line which wasn't there before. This is the **commit hash** which acts as a unique identifier for the commit. Any time we need to refer to a specific commit we use this hash. Typically we only need to provide the first seven characters when doing so. + +Commits are listed with the most recent first and you can exit the log by pressing the `q` key. + +{{}} +It's time to practice using the CLI by recreating the next stage of our original Git notes. + +1. Create a new file called planning.txt +2. Add some text to it +3. Save it +4. Stage your changes +5. Make a commit with the message "Add project planning document" +6. Check the history to see both commits +{{}} \ No newline at end of file diff --git a/common-content/en/module/git-cli/branches/index.md b/common-content/en/module/git-cli/branches/index.md new file mode 100644 index 000000000..b236eb098 --- /dev/null +++ b/common-content/en/module/git-cli/branches/index.md @@ -0,0 +1,121 @@ ++++ +title = 'Branching & Merging' + +time = 20 +[objectives] + 1='Create a branch in a repository from the terminal' + 2='Merge two branches in the terminal' + 3='Delete a branch' +[build] + render = 'never' + list = 'local' + publishResources = false + ++++ + +By now you have had lots of experience creating branches and raising pull requests on GitHub. When it comes to merging work we won't make any fundamental changes: we will still use Github to manage PRs and to complete any merges when working on group projects. Pull requests are a feature of GitHub specifically rather than Git so we can't recreate them exactly using the terminal, but in this section we'll look at how we can create and merge branches. + +### Creating a branch + +For this section we'll revisit the educational blog from the [JavaScript Fundamentals module](/itp/javascript-fundamentals/sprints/1/prep/). Open the project in VSCode and navigate to that directory in your terminal. + +We're going to follow a similar workflow to begin with by creating a branch. For this we're going to use the `git branch` command along with the name we want to give our branch. Since this will be our second update we'll call it `update-blog-2`. + +```sh {title="education-blog"} +git branch update-blog-2 +``` + +At this point we diverge from how VSCode handled the process because we haven't actually switched to our new branch, only created it. We can confirm this by typing `git branch` without any arguments and checking the output. + +```console +* main + update-blog-1 + update-blog-2 +``` + +We have our `main` branch, our new branch and `update-blog-1` from our previous Git work. The asterisk indicates which branch we are currently working on. If we want to work on our new branch we can move over to it using `git switch`. + +```sh {title="education-blog"} +git switch update-blog-2 +``` + +Checking `git branch` will confirm the change. + +```console + main + update-blog-1 +* update-blog-2 +``` + +If we know we're going to switch immediately we can do both steps at once by adding the `-c` flag to the switch command and providing the name of the branch. + +```sh +git switch -c update-blog-2 +``` + +{{}} +Add some more unblocking tips to the list and commit your changes. +{{}} + +### Merging + +With the tools we have had at our disposal so far, at this point we would publish our branch to Github and raise a pull request. Once everything was reviewed and merged we would pull the updated version of `main` and continue. To be clear, **this is still the recommended way of working!** + +It's not the only way of working though. We can use `git merge` to complete the merge locally and bypass Github. This has its drawbacks though, in particular the fact it is only possible to get your changes reviewed if your colleague is in the room with you. + +We need to think carefully about how we manage the merge. We don't want to end up with any broken code on our `main` branch, so if there are any issues it's better to sort them out before they get there. How will we know there are going to be issues? + +Before merging our branch onto `main` we can discover any conflicts by merging `main` onto our branch first. If there are problems (such as merge conflicts) we fix them on the branch and leave `main` unpolluted for everyone else. Once the conflicts are resolved we merge our changes to `main`. + +First we need to ensure we are on the correct branch. Use `git branch` to check if you are unsure. Then use `git merge` and the name of the branch you want to merge. + +```sh {title="education-blog"} +git merge main +``` + +We are asking Git to merge the _named_ branch onto the _current_ branch. If there were commits on `main` which we did not have on `update-blog-2` they would be moved across, but since there aren't we will see a message confirming this. + +```console +Already up to date. +``` + +The next step is switching to `main`: + +```sh {title="education-blog"} +git switch main +``` + +Pause for a moment and look at the files in VSCode - see how the new tips you added have disappeared? That commit only exists on `update-blog-2`, so the changes we made don't show up on `main` yet. We can confirm this by checking `git log` and seeing that the commit isn't listed. + +Next merge our working branch: + +```sh {title="education-blog"} +git merge update-blog-2 +``` + +This time we do have some commits to merge so we get a summary of the changes: + +```console +Updating 4244c24..438484a +Fast-forward + blogs/1.md | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) +``` + +And now our changes are visible in VSCode while we're on `main`. + +### Deleting branches + +When working on longer projects we will likely end up with a _lot_ of branches. This can get very confusing very quickly so we're going to practice good Git hygiene by deleting branches we no longer need. + +We're going to use the `git branch` command again but this time we're going to add a flag. By including `-d` before a branch name we will delete the branch locally, but **not** on Github. Likewise if we delete a branch on Github the local version will remain. Let's delete `update-blog-2` since we're done updating our list for now. + +```sh {title="education-blog"} +git branch -d update-blog-2 +``` + +Checking with `git branch` will confirm the branch is gone. + +{{}} +Like most other things in the terminal, **this is permanent!** Make sure you're _definitely_ done with the branch before deleting it. +{{}} \ No newline at end of file diff --git a/common-content/en/module/git-cli/initialisation/index.md b/common-content/en/module/git-cli/initialisation/index.md new file mode 100644 index 000000000..b67014087 --- /dev/null +++ b/common-content/en/module/git-cli/initialisation/index.md @@ -0,0 +1,64 @@ ++++ +title = 'Working with Git in Terminal' + +time = 10 +[objectives] + 1='Initialise a Git repository from the terminal' +[build] + render = 'never' + list = 'local' + publishResources = false + ++++ + +So far you have seen two ways of interacting with your file system: your computer's terminal application and its explorer GUI. You probably have a preference and you probably prefer using one over the other for particular tasks. Neither is the "correct" way of working, but it's useful to know about both. + +We have similar options for Git. So far we have been working with VSCode's version control tools and they're fine for what we need, but there are some things that are quite fiddly and some things we we can't do at all. In this sprint we'll look at using Git in the terminal and recreate our VSCode workflow, looking at some of the differences as we go. + +### Preparation + +We're going to recreate our workflow from the Onboarding module and sprint 1 of JavaScript Fundamentals exactly. We'll create the same files and the same commits, but this time we won't touch the source control tab at all. + +Start by opening your terminal and creating a directory to work in. Call this one `git-cli-practice` + +```sh +mkdir git-cli-practice +``` + +### Initialising a repository + +Everything we did using buttons in VSCode can be recreated using terminal commands. Think back to working with packages last sprint: we knew we were doing something with npm because the commands we used all started with `npm`. In a similar way our Git commands will all start with `git`. + +We initialise a new repository using the `init` command in our directory. + +```sh {title="git-cli-practice"} +git init +``` + +Our directory is now a local git repository. We can confirm this using the `status` command. + +```sh {title="git-cli-practice"} +git status +``` + +This should give the following output: + +```console +On branch main + +No commits yet + +nothing to commit (create/copy files and use "git add" to track) +``` + +We are ready to start committing! + +{{}} +Some terminal applications will give a visual indication when the current directory is a git repository or can be customised to do so. For example, it may include the word "git" and the name of the current branch next to the directory name. Check the documentation for your OS and terminal app to find out how to set this up if you want to. +{{}} + +What have we actually done here? The `ls -a` command will list files and folders including anything which has been hidden and if we run it here we will see that a `.git` folder has been created. This is the directory Git uses to store our commit history and everything else it needs to do its job. + +{{}} +Initialising a repository in the wrong place is a common mistake to make when learning how to use Git in the terminal. It can cause some tricky problems, but it isn't difficult to fix. Just delete the `.git` folder with `rm -r .git` and the repository will be deleted. Remember that this is permanent though - the files will remain but the commit history showing how they changed will be gone. +{{}} \ No newline at end of file diff --git a/common-content/en/module/git-cli/pushing-pulling/index.md b/common-content/en/module/git-cli/pushing-pulling/index.md new file mode 100644 index 000000000..5b9521550 --- /dev/null +++ b/common-content/en/module/git-cli/pushing-pulling/index.md @@ -0,0 +1,60 @@ ++++ +title = 'Pushing & Pulling' + +time = 20 +[objectives] + 1='Push to a remote repository from the terminal' + 2='Configure a default upstream' +[build] + render = 'never' + list = 'local' + publishResources = false + ++++ + +Our repositories are linked and it's time to push our changes. Once again we can condense many button clicks down to a single command in the terminal. + +### Pushing changes + +The command we will use is `git push`, but it needs two extra pieces of information: + +- The remote we are pushing to +- The branch we are pushing + +We only have one of each at the moment so our command will be pretty straight-forward. We'll see how to push a different branch in a later section. + +```sh {title="git-cli-practice"} +git push origin main +``` + +This will take every commit on `mian` which has not yet been pushed and upload it to the url specified as `origin`. In our case this will be GitHub and if we check the repository now we will see our files there, just like when we used VSCode. + +{{}} +1. Create a file called `facts.txt` +2. Add your favourite fun fact to the file +3. Save the file +4. Commit your changes +5. Add another fact. Commit this change. +6. Push to GitHub +7. Go to GitHub and refresh to see your changes! +{{}} + +### Default upstream + +When we push to GitHub we are pushing to an **upstream** branch. If we are regularly pushing to the same branch we can configure a default so that we only need to type `git push` without the remote or branch name. **Handle with care!** When we are working in the terminal we don't have any of the safety features VSCode has and it would be very easy to accidentally push something to the wrong place if we rely on a default. + +We can set a default by using the `-u` flag when we push. + +```sh +git push -u origin main +``` + +### Pulling + +The commands to pull are similar but use the `pull` keyword instead of `push`. + +```sh +git pull origin main +``` + +It is possible to pull one branch from GitHub onto another locally, eg. pull the remote `main` onto the local `my-feature-branch`. This can lead to conflicts though and is best avoided in favour of managing branches properly. \ No newline at end of file diff --git a/common-content/en/module/git-cli/remote-repositories/index.md b/common-content/en/module/git-cli/remote-repositories/index.md new file mode 100644 index 000000000..fa487af04 --- /dev/null +++ b/common-content/en/module/git-cli/remote-repositories/index.md @@ -0,0 +1,39 @@ ++++ +title = 'Remote Repositories' + +time = 10 +[objectives] + 1='Link local and remote repositories using the terminal' +[build] + render = 'never' + list = 'local' + publishResources = false + ++++ + +The next step in the process is linking our repository to a remote so we can push our changes to GitHub. + +{{}} +Create a new repository on GitHub called `git-cli-practice`. This part of the process happens entirely on GitHub so will be exactly the same as it was when we first looked at this. +{{}} + +### Connecting a remote + +When we want to do something with a remote through terminal we will use the `git remote` command. First we need to `add` the remote to our repository. Copy the url for your repository from GitHub and use it with teh command below: + +```sh {title="git-cli-practice"} +git remote add origin https://github.com/your-username/git-cli-practice.git +``` + +- `git remote` tells us we are using a Git command specifically for managing remotes +- `add` tells us we are adding a new remote +- `origin` is the name we attach to the remote +- the final part is the url + +{{}} +If you are using HTTPS as your protocol you will be asked to enter your username and password _every_ time you push commits to GitHub. If you haven't already [configured SSH](https://docs.github.com/en/authentication/connecting-to-github-with-ssh) now would be an excellent time to do so! +{{}} + +We can create as many remotes as we like so long as they have unique names. By convention we will keep using `origin` as the name for our remote source control repo. We can see a list of all available remotes by typing `got remote` with no other arguments. + +Occasionally we may need to disconnect a remote from our local repository. If that ever happens we can use the command `git remote remove {remote_name}` to break the connection. \ No newline at end of file diff --git a/common-content/en/module/js1/anonymous-functions/index.md b/common-content/en/module/js1/anonymous-functions/index.md index 6f19505b6..7e41da354 100644 --- a/common-content/en/module/js1/anonymous-functions/index.md +++ b/common-content/en/module/js1/anonymous-functions/index.md @@ -19,37 +19,26 @@ function convertToPercentage(decimalNumber) { } ``` -In our Jest test, we wrote a function differently: +In our tests we wrote the functions differently: ```js -function() { - expect(getOrdinalNumber(1)).toEqual("1st"); - expect(getOrdinalNumber(11)).toEqual("11th"); - expect(getOrdinalNumber(21)).toEqual("21st"); +function(){ + assert.equal(formatAs12HourClock("23:00"), "11:00 pm"); } ``` +Note the difference between the two: we didn't give a name to the function in our test. -{{}} - -Stop and identify the difference in syntax between these two function definitions. - -{{}} - -We didn't give a name to the function in our Jest test. - -This is ok, because we don't need it to have a name. We don't call the function by name. We passed the function as an {{}}Arguments are values given to a function which can be different every time we call the function.{{}} to the `test` function. The `test` function takes the function as a {{}}A parameter is a named variable inside a function. The variable's value is given by the caller, when the function is called.{{}}. And function parameters get their own names in the {{}}Scope is where a variable can be accessed from. When we define function, its parameters are only available inside the function.{{}} of the function. +This is ok, because we don't need it to have a name. We don't call the function by name. We passed the function as an argument to the `test` function. When we execute the code Node will create its own label internally and use that when it needs to reference the function. We can imagine the `test` function is defined like this: ```js -function test(name, testFunction) { +function test(label, testFunction) { // Call the passed test function testFunction(); } ``` -Inside `test` our function is labelled with the name `testFunction`. It would be labelled this whatever we named it before. Even if we didn't label it ourselves at all, it is still labelled with the name `testFunction` inside `test`. - -Because it doesn't matter what we named the function (because we never call it by name), we didn't give it a name. +The internal label attached to the function by Node doesn't matter because the function will only ever be called by Node. We will never need to use it again outside of this test. Otherwise, these two functions act the same. The only difference between them is whether we created a variable name for the function in the scope where we defined it. diff --git a/common-content/en/module/js1/arrow-functions/index.md b/common-content/en/module/js1/arrow-functions/index.md index 089cb4980..b888d6df2 100644 --- a/common-content/en/module/js1/arrow-functions/index.md +++ b/common-content/en/module/js1/arrow-functions/index.md @@ -1,9 +1,10 @@ +++ title = 'Arrow functions' -time = 5 +time = 20 [objectives] 1='Write an arrow function' + 2="Call a function which has been stored in a variable" [build] render = 'never' list = 'local' @@ -11,9 +12,11 @@ time = 5 +++ -As we write more code, we are going to write lots and lots of {{}}An anonymous function is a function which is not bound to a name in the scope where it is defined.{{}}. +As we progress through this course we will find lots of situations where we can use anonymous functions. In this section we'll see how we can make them even shorter by removing the `function` keyword and in some cases reducing everything to a single line. -JavaScript has even shorter ways of writing an anonymous function. These four functions all do the same thing: +### Types of functions + +We have already seen lots of examples of **named functions**. These are functions defined like we did in the previous module. ```js function convertToPercentage(decimalNumber) { @@ -21,46 +24,70 @@ function convertToPercentage(decimalNumber) { } ``` +In the last section we introduced the concept of **anonymous functions** where we don't need to assign a name to the function. + ```js -// We can skip the name of the function if we don't need it to have a name. function (decimalNumber) { return `${decimalNumber * 100}%`; } ``` +The `function` keyword isn't the only way for us to define a function. In modern versions of JavaScript we can leave it out, but we still need a way of linking the list of parameters to the function body. We use an arrow symbol (`=>`) to do so and this is why we call anonymous functions defined this way **arrow functions**. + ```js -// We can also skip the keyword 'function'. -// If we do this, we need an arrow between our parameters and the function body. (decimalNumber) => { return `${decimalNumber * 100}%`; }; ``` +When using arrow functions we can go a step further and omit the braces and `return` keyword too. This is called an **implicit return** but it can only be used when the function body contains a single expression. + ```js -// If our function just returns a single value, -// without needing any other statements in our function, -// we can even skip the return keyword. (decimalNumber) => `${decimalNumber * 100}%`; ``` This can make it easier and quicker to write functions. It also reduces the number of things we need to read in a function. -Applying all of these techniques, we can rewrite our Jest test with fewer words: +{{}} +Rewrite your tests in `timeConverter.test.js` to use arrow functions. -```js -test("works for any number ending in 1", () => { - expect(getOrdinalNumber(1)).toEqual("1st"); - expect(getOrdinalNumber(11)).toEqual("11th"); - expect(getOrdinalNumber(21)).toEqual("21st"); -}); +
+Solution: + +```js {title="timeConverter.test.js"} +test("correctly convert time after 12:00", () => assert.equal(formatAs12HourClock("23:00"), "11:00 pm")); + +test("can correctly convert morning time", () => assert.equal(formatAs12HourClock("08:00"),"08:00 am")); + +test("can correctly convert midnight", () => assert.equal(formatAs12HourClock("00:00"),"12:00 am")); ``` -It doesn't matter whether you use arrow functions or use the `function` keyword - they work the same. +We can use the implicit return syntax here because the `assert.equal()` call is the only expression in the function body. -Not all arrow functions are anonymous - you can assign them to a variable too: +
-```js -const convertToPercentage = (decimalNumber) => `${decimalNumber * 100}%`; +{{
}} + +### Assigning functions to a variable + +Our anonymous functions don't need to stay anonymous - we can assign them to a variable if we need to. When we want to call the function we can do so using the variable name, just like we would if it was a named function. + +Create a new file to try this in. + +```js {title="modifyingNumbers.js"} +const doubleNumber = function(number){ + return number *2; +} + +const halfNumber = (number) => number / 2; + +console.log("doubled number:", doubleNumber(2)); +console.log("halved number:", halfNumber(2)); ``` -Anonymous vs named refers to whether the function is bound to a name, not whether it was defined with the `function` keyword or an `=>`. +Running the file prints: + +```console +doubled number: 4 +halved number: 1 +``` diff --git a/common-content/en/module/js1/cases/index.md b/common-content/en/module/js1/cases/index.md deleted file mode 100644 index 462e4d6e6..000000000 --- a/common-content/en/module/js1/cases/index.md +++ /dev/null @@ -1,122 +0,0 @@ -+++ -title = 'First test case' - -time = 40 -[objectives] - 1='Outline the effect of running npm test' - 2='Interpret documentation to determine how part of a third-party API behaves' - 3='Describe what toEqual checks in the Jest library' - 4='State the current return value of a function and the target output for a given test' - 5='Implement a test case to describe the behaviour of a function' - -[build] - render = 'never' - list = 'local' - publishResources = false - -+++ - -> 🎯 Goal: Write a test for the case below, using Jest: - -#### Case 1 πŸ’Ό - -Our first case is that the ordinal number for `1` should equal `"1st"`. - -We can create a file called `get-ordinal-number.test.js` and write our first test there. -We can use [documentation](https://jestjs.io/docs/getting-started) to work out how to write our first test using Jest. - -`get-ordinal-number.test.js`: - -```js -test("converts 1 to an ordinal number", function () {}); -``` - -Let's break down this syntax. - -The `test` function is part of the Jest API, a function we use to perform a particular task. -In particular, we're using `test` to create a test case. -Before, we could use `Math.round` and `console.log` because `Math` and `console` are provided for us by Node. - -`test` isn't provided by Node, but when we ask Jest to run our tests, it will make sure the `test` function exists and that our code can use it. - -Let's break down the arguments we're passing to `test`: - -- 1st argument: `"converts 1 to an ordinal number"`, a string which describes the behaviour we're testing for -- 2nd argument: `function() {}`, we will write some assertions in this `function() {}` to check the behaviour - -### βš–οΈ Creating assertions - -We need to write an **assertion** inside the body of `function() {}` inside `get-ordinal-number.test.js` - -`get-ordinal-number.test.js`: - -```js -test("converts 1 to an ordinal number", function () {}); -``` - -{{}} -The assertion is the part of the test code that checks if a particular thing is true or not. -{{}} - -In this example, we want to check that the following is true: - -We expect `getOrdinalNumber(1)` to be `"1st"` - -An assertion in Jest looks like this: - -```js -expect(currentOutput).toEqual(targetOutput); -``` - -The function `toEqual` is used to check that the current output of `getOrdinalNumber(1)` and the target output of `"1st"` are equal to each other. - -`toEqual` is just one example of a function called a [matcher](https://jestjs.io/docs/using-matchers). -A matcher is a function we use to compare values in Jest. - -So the whole test looks like this: - -```js -test("converts 1 to an ordinal number", function () { - expect(getOrdinalNumber(1)).toEqual("1st"); -}); -``` - -### πŸ‘Ÿ Running tests - -We can try running the file `get-ordinal-number.test.js` with node in the following way: - -```bash -node get-ordinal-number.test.js -``` - -but we get an error: - -```bash -ReferenceError: test is not defined -``` - -Googling "ReferenceError JavaScript", [MDN tells us this is because we're referring to a variable that doesn't exist](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ReferenceError). This is because `test` isn’t defined anywhere in the file. - -We need to execute this file so that the Jest API is available in our file. We can do this by running the test file using Jest: we do this using an npm script. - -The "scripts" section of the `package.json` is where we can write useful commands we'll use in our project. We can add a "scripts" section to the `package.json` so that it reads as follows: - -```json {linenos=table,hl_lines=["4-6"],linenostart=1} -{ - "name": "week-4-test-example", - "description": "An example application showing how to write tests using the jest framework", - "scripts": { - "test": "jest" - }, - "devDependencies": { - "jest": "^29.5.0" - } -} -``` - -Finally, we'll need to run our tests. -Now we can run the command `npm test`. - -When we execute the command, `npm test`, we will run `npm`, and `npm` will look inside the "scripts" section of the `package.json` and look up the command for "test" - in this case, "jest". `npm` will then run "jest". - -We can't ourselves just run `jest` on the command line, because it isn't installed in a place our terminal knows about. But when `npm` runs a script, it will make sure all dependencies installed for the project are available. diff --git a/common-content/en/module/js1/clocks/index.md b/common-content/en/module/js1/clocks/index.md deleted file mode 100644 index 6c92fb788..000000000 --- a/common-content/en/module/js1/clocks/index.md +++ /dev/null @@ -1,43 +0,0 @@ -+++ -title = '12 vs 24 hour clock' - -time = 10 -hide_from_overview = true -[objectives] - 1='Identify a pattern between a set of inputs and outputs produced by a given function' -[build] - render = 'never' - list = 'local' - publishResources = false - -+++ - -We usually write [the time](https://www.bbc.co.uk/bitesize/topics/zkfycdm/articles/z44mqfr) in one of two ways: the analogue 12 hour clock or the digital 24 hour clock. The 12 hour clock counts up to 12: it resets at midday. The 24 hour clock counts up to 24: it resets at midnight. - -{{}} - -| 2️⃣4️⃣ hour time | πŸ•› 12 hour time | -| -------------- | --------------- | -| 09:00 | 09:00 am | -| 10:00 | 10:00 am | -| 11:30 | 11:30 am | -| 12:00 | 12:00 **pm** | -| 13:43 | 1:43 **pm** | -| 14:00 | 2:00 **pm** | - -{{}} - -We use the notation "HH:MM". HH is our stand-in for the hours value. MM is our stand-in for the minutes value. - -## 🧩 Stating the problem - -Let's pose a problem: given any time in 24 hour clock, we want to format it as a 12 hour clock time. To achieve this goal, we're going to implement a function `formatAs12HourClock`. - -_Given_ a time in 24 hour clock -_When_ we call `formatAs12HourClock` -_Then_ we get back a string representing the same time in 12 hour clock. - -### πŸ§ͺ Our tests: - -I expect `formatAs12HourClock("09:00")` to be `"09:00 am"` -I expect `formatAs12HourClock("14:19")` to be `"2:19 pm"` diff --git a/common-content/en/module/js1/installing/index.md b/common-content/en/module/js1/installing/index.md deleted file mode 100644 index 88c611be9..000000000 --- a/common-content/en/module/js1/installing/index.md +++ /dev/null @@ -1,103 +0,0 @@ -+++ -title = 'Installing Jest' - -time = 20 -[objectives] - 1='Outline the effects of running an installation command, e.g. npm install' - 2='Install a dependency with npm' -[build] - render = 'never' - list = 'local' - publishResources = false - -+++ - -Jest is a package used to help us to write and run test cases in JavaScript. -Our next step will be to figure out how to install the Jest package on our machine, so that we can use it in our project. - -We can find out more about the Jest framework from the [documentation online](https://jestjs.io/docs/getting-started). - -In the **Getting started** section of the documentation, Jest gives us the following command: - -```console -npm install jest --save-dev -``` - -Let's break down the different parts of this command. - -- `npm` - `npm` is the package management tool we are using, so we need to run it. - -- `install` - `npm` has a subcommand called `install`. We use it to download a package from the [**npm** registry](https://www.npmjs.com/) onto our machine and install it. - -- `jest` - this is the name of the package we want to install on our machine. - -- `--save-dev` - this means the package is needed for development but _not_ needed in production. Our ordinal app doesn't need `jest` to run, but we need it to help us develop it. - -So overall we can think of this command as saying: -_"Please go to the npm database, find the Jest package and install it on my local machine"_ - -Let's execute this command in the same directory as the `package.json`. -To double check we're in the correct directory, we can run `pwd`: - -```console -$ pwd -.../{{}}/ordinal-testing-example -``` - -`pwd` is telling us we're in the `ordinal-testing-example` directory. - -We need to double check the `package.json` is also there too. - -```console -$ ls -package.json -``` - -Now we can execute the command - -```bash -npm install --save-dev jest -``` - -Our project structure will now look as follows: - -```raw -ordinal-testing-example -β”œβ”€β”€ node_modules -β”œβ”€β”€ package-lock.json -└── package.json - -1 directory, 3 files -``` - -After running the command, we now have a directory called `node_modules` in our project too. - -> The `node_modules` directory contains all the code from the{{}}A **dependency** is a package that your project depends upon.{{}}we installed in our project. You won't need to look inside the `node_modules` directory - you just need to know it contains the code for Jest and any other dependencies we install in our project. - -Running the `npm` command also updated our `package.json` file for us: - -```json -{ - "name": "week-4-test-example", - "description": "An example application showing how to write tests using the jest framework", - "devDependencies": { - "jest": "^29.5.0" - } -} -``` - -We've now got some additional information inside the `package.json`: - -```json -"devDependencies": { - "jest": "^29.5.0" -} -``` - -{{}} - -### - -Install Jest on your local machine. Double check you've got the correct files and folders written to your local machine. - -{{}} diff --git a/common-content/en/module/js1/jest/cases/index.md b/common-content/en/module/js1/jest/cases/index.md new file mode 100644 index 000000000..8e3ba4900 --- /dev/null +++ b/common-content/en/module/js1/jest/cases/index.md @@ -0,0 +1,103 @@ ++++ +title = 'Testing with Jest' + +time = 40 +[objectives] + 1='Define test cases using Jest' +[build] + render = 'never' + list = 'local' + publishResources = false + ++++ + +Let's revisit `formatAs12HourClock()` and test it using Jest. + +### Defining a test + +We're going to use Jest's `test()` function to define our test. Jest is a little different from other packages in that we don't need to import the functions to be able to use them. + +Every time we use `test()` we need to pass it two arguments: +- A string describing what we're testing +- A function where we will call the function we are testing and define the expected outcome + +We'll start by providing the string and an empty function. + +```js {title="timeConverter.test.js"} +import {formatAs12HourClock} from "./timeConverter"; + +test("correctly convert time after 12:00", () => { + // TODO +}); +``` + +Inside the function we are going to use two more functions from Jest: +- `expect()` will be used to call the function we are testing and capture the _actual_ value returned +- `toEqual()` will be used to provide the _expected_ value + +```js {title="timeConverter.test.js"} +import {formatAs12HourClock} from "./timeConverter"; + +test("correctly convert time after 12:00", function(){ + expect(formatAs12HourClock("23:00")).toEqual("11:00 pm"); +}); +``` + +When we run our test: +1. The value `"23:00"` will be passed to `formatAs12HourClock` +2. The code in the function will be executed and the returned value will be stored as the _actual_ value by `expect()` +3. The _actual_ value will be compared to the _expected_ value passed to `toEqual()` +4. The test will **pass** if the two values match. If they don't it will **fail**. + + +### Running the test + +If we try to run `timeConverter.test.js` using Node we'll get an error. That's because Jest isn't designed to be run in the same way as a typical program, we'll need to use npm to help us out. + +Take a look at `package.json` and you'll see a `scripts` property with a nested object as its value. We can define scripts which can execute larger processes when we type `npm run {scriptName}`. We already have a value defined for `test`: + +```json {title="package.json"} +{ + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} +``` + +Try running it by typing `npm test` in the terminal and see what happens: + +```console +npm test + +Error: no test specified +``` + +This is a useful default, but now that we have a test we don't want to see an error message when we try to run it. Replace the string associated with `test` with the one shown below: + +```json {title="package.json"} +{ + "scripts": { + "test": "node --experimental-vm-modules ./node_modules/.bin/jest" + } +} +``` + +Remember to also update the `type` value to `"module"`. + +Now try running `npm test` again. This time the test should run successfully and log the results to the terminal. You should see the string you passed to `test()` copied there with a check mark beside it to indicate that the test passed. Success! + +{{}} +The `toEqual()` function is an example of a **matcher**. Using the [Jest documentation](https://jestjs.io/docs/using-matchers) read about some other matchers which are available and identify which one would be most appropriate to use in each of these tests: +1. Checking if a function returns a value above a given minimum +2. Adding two decimal numbers +3. A function's return value **isn't** `null` + +
+Solutions: + +1. `toBeGreaterThan()` +2. `toBeCloseTo()` +3. `not.toBeNull()` + +
+{{
}} \ No newline at end of file diff --git a/common-content/en/module/js1/jest/installing/index.md b/common-content/en/module/js1/jest/installing/index.md new file mode 100644 index 000000000..57c7cbc96 --- /dev/null +++ b/common-content/en/module/js1/jest/installing/index.md @@ -0,0 +1,51 @@ ++++ +title = 'Using a Testing Library' +time = 20 +[objectives] + 1='Explain why we need to use testing libraries' + 2='Explain the difference between `dependencies` and `devDependencies`' +[build] + render = 'never' + list = 'local' + publishResources = false + ++++ + +Last sprint we wrote our first unit tests using the assertion libraries built in to Node. They did the job for us, but they can't do everything. There will be times when we need to bring in specialised tools to help. + +In this section we will look at how we can test our code using a **testing framework**. We're going to use Jest, which is one of the most popular JavaScript testing frameworks. We can find out more about Jest from the [documentation](https://jestjs.io/docs/getting-started). We're going to recreate the tests we wrote last sprint using Jest and see how it compare to using `node:test`. + +### Installing Jest + +Before we can start using Jest we need a fresh directory to work in. + +- Create a new directory called `testing-with-jest`. Make you are **outside** the `packages-practice` directory. +- Copy the `timeConverter.js` file from the last sprint into this directory. +- Create a new file called `timeConverter.test.js` +- Import `formatAs12HourClock()` into the test file + +We're going to install Jest using npm. First we need to use `npm init -y` to create `package.json` like before, then we install Jest. There's going to be a slight difference this time though: + +```sh {title="username/cyf-work/time-conversion"} +npm init -y +npm install --save-dev jest +``` + +This time we have included the `--save-dev` flag with the install command. Let's see what that changed in `package.json`: + +```json {title="package.json"} +{ + // ... + "devDependencies": { + "jest": "^30.5.0" + } +} +``` + +This time we have a `devDependencies` key instead of `dependencies`. There won't be a difference in terms of how we use the packages while we are writing code, but the two are handled differently when the time comes to deploy our code. Certain dependencies support core parts of our program, such as checking if a number is odd in the [previous example](itp/testing/sprints/2/prep/#using-a-package). Others are only useful while we are still developing. Testing falls into the second category: our end users won't need to run the tests when they have the finished app in front of them. Those dependencies are marked as `devDependencies`. + +### Version numbers + +Every dependency we install has an associated version number. In this example we have installed version `30.5.0` of Jest. If a new version of a package is released these digits will change and npmjs has an[article explaining what each digit represents](https://docs.npmjs.com/about-semantic-versioning). It's important to keep a record of which version of a package we have used in development. + +That applies to our packages' dependencies too, which is where the `package-lock.json` file comes in. This keeps track of the version numbers of _every_ dependency in our tree so we can exactly recreate the structure of our program later, even if something in the middle of the tree receives an update. diff --git a/common-content/en/module/js1/installing/jest-install.png b/common-content/en/module/js1/jest/installing/jest-install.png similarity index 100% rename from common-content/en/module/js1/installing/jest-install.png rename to common-content/en/module/js1/jest/installing/jest-install.png diff --git a/common-content/en/module/js1/testing/failing-tests/index.md b/common-content/en/module/js1/testing/failing-tests/index.md new file mode 100644 index 000000000..c4b176424 --- /dev/null +++ b/common-content/en/module/js1/testing/failing-tests/index.md @@ -0,0 +1,107 @@ ++++ +title = 'Failing Tests' + +time = 45 +[objectives] + 1="Interpret the output when a test fails" + 2='Modify a function in response to a failing test' +[build] + render = 'never' + list = 'local' + publishResources = false + ++++ + +We have written a test and it passed, so can we say that our function works? + +The answer is no! We have only tested one aspect of our function: converting an afternoon time. We need to check that it works for morning times as well. We're going to need a second test. + +{{}} +Write another test to check that `"08:00"` will be correctly converted to `"08:00 am"` + +
+Solution: + +```js {title="timeConverter.test.js"} +test("can correctly convert morning time", function(){ + assert.equal(formatAs12HourClock("08:00"),"08:00 am"); +}); +``` + +
+{{
}} + +We have a problem when we run the test though - it fails! How can we make sense of the output and figure out what we need to fix? + +### Interpreting the output + +The first change we see is on the second line of the output. We still have our previous test with a check mark next to it but now we also have our new test with a cross. This tells us which test has caused the failure. We also see some summary statistics telling us how many tests have passed or failed in total. + +{{}} +Testing frameworks doesn't stop when a test fails, they carry on and runs every test in the file. Each failed test will have a log similar to the one in front of us now. It can get confusing when we have lots of failed tests but the summary at the start of the log will help us identify them. +{{}} + +The next section tells us exactly what has caused our test to fail. + +```console + AssertionError [ERR_ASSERTION]: '8 am' == '08:00 am' +``` + +This is an example of an **assertion error**. Our function is returning a value, but not the one that it should be. Recall from the last section that our _actual_ value is what is returned to us by the function and in this example it's `"8 am"`. The _expected_ value was `"08:00 am"`. Our function is returning the wrong thing. + +### Fixing the bug + +It can be surprisingly hard to identify the root cause of an assertion error. It could be the case that there is a flaw in our logic, for example a condition in an `if`-statement is not defined correctly, but it could just as easily be a typo. We should start by examining the two values and seeing if there are any obvious errors to fix. + +{{}} +Your backlog tasks this week include an exercise using VSCode's built-in debugging tools (TODO: link to the ticket after it's been moved). These are very useful in situations like this where we need to observe how values change as we progress through a program. +{{}} + +- **The numbers match**, which indicates that we aren't accidentally subtracting 12 from the value. +- **We have added the correct suffix**. We have "am" at the end of t string, which means we followed the correct branch of the `if`-statement. +- **Spacing and casing are correct**, so we haven't made a typo formatting the string. + +None of these checks are a guarantee that there _isn't_ a problem with any of these steps, but they do suggest that the problem is somewhere else. + +If we look closely at the output we see that the main difference is that the _actual_ output is missing the `:00` part of the string. Compare the two branches of the `if`-statement: In the first branch we add `:00 pm` to the value of `hours` but in the second we only add `am`. Update the second branch: + +```js {title="timeConverter.js"} +function formatAs12HourClock(time) { + + const hours = Number(time.slice(0, 2)); + + if (hours > 12) { + return `${hours - 12}:00 pm`; + } + return `${hours}:00 am`; +} +``` + +We still aren't quite there! Our values are closer to matching but still not quite there. We're still missing a leading `0` from the _actual_ value. + +This is where the debugging tools would be particularly useful. Without them we can't see what's happening inside the function while it runs, but by adding a breakpoint we would be able to check that the value of `hours` is actually what we think it is. In this case it is `8` rather than `08`, so we insert the wrong value into the string literal. + +{{}} +Think back to your research on `Number()` earlier in the sprint. What did you find out about it? Can you find anything in the documentation that would explain why lose the first digit in this case, but it worked in the first test? + +
+ Solution: + The `Number()` converts a string into a number, but this isn't always straight-forward. When we call the `.slice()` function we extract the first two characters of the string representing the time. For `"23:00"` this was `"23"` and everything was fine, but for `"08:00"` it is `"08"`. We don't usually write numbers with a leading 0, so what should `Number()` do with it here? It simply ignores it, returning the value `8` that we are more familiar with. +
+{{
}} + +We could now write some complex logic to add a `0` back to the front of the string if we have a single-digit number, but before we do that we should revisit our list of requirements. If we look back at what we defined when we wrote the function we see that **we don't need to change the value** if it is before 12:00. Writing the logic would be unnecessary. Instead we can simply append "am" to the value passed into the function. + +```js {title="timeConverter.js"} +function formatAs12HourClock(time) { + + const hours = Number(time.slice(0, 2)); + + if (hours > 12) { + return `${hours - 12}:00 pm`; + } + return `${time} am`; +} +``` + +Both tests now pass. It's important to run all of our tests whenever we make changes to the code, even if we have only been editing a small part of it. It can be difficult to predict how changes in one function will affect the behaviour of others and tests will help us spot any side-effects. \ No newline at end of file diff --git a/common-content/en/module/js1/testing/first-test/index.md b/common-content/en/module/js1/testing/first-test/index.md new file mode 100644 index 000000000..ab544c7c1 --- /dev/null +++ b/common-content/en/module/js1/testing/first-test/index.md @@ -0,0 +1,98 @@ ++++ +title = 'Writing Our First Test' + +time = 40 +[objectives] + 1='Export a function from a file' + 2='Implement a test case to describe the behaviour of a function' +[build] + render = 'never' + list = 'local' + publishResources = false + ++++ + +It's time to write our first test! We're going to start off by checking something we know already works: `formatAs12HourClock("23:00")`. + +{{}} +We wouldn't usually test our code by writing the tests _after_ we have written the code. We're doing it here so that we're only covering one new concept at a time, but in practice it can lead to us writing tests which just tell us what we want to hear. + +Instead developers aim to write the tests first according to the product specification, then write the code to make the tests pass. This is called **test-driven development** and we'll look at it in the next sprint. +{{}} + +We're going to need a file to write our tests in. Create a new file called `timeConverter.test.js`. + +{{}} +As your projects get bigger you will likely want to separate your testing files into a separate `testing` directory to keep things organised +{{}} + +We need to access our function from our test file which means we'll need to `import` it, but before we can do that we need to make it accessible using `export`. Add the following line to the bottom of `timeConverter.js`: + +```js {title="timeConverter.js"} +// ... +export {formatAs12HourClock}; +``` +Now we can import it at the top of our test file using the `import` keyword: + +```js {title="timeConverter.test.js"} +import {formatAs12HourClock} from "./timeConverter.js"; +``` + +Now we can call the function from within `timeConverter.test.js`, even though it is defined somewhere else. We can `export` and `import` multiple functions at the same time by comma-separating them inside the braces. + +### Testing tools + +Node has some built-in tools which can help us with our testing. Using third-party tools like this is common practice for developers, otherwise we would need to write our own. By using industry-standard tools we give other developers confidence in our tests and in our code. In the next sprint we'll see some other examples. + +We need to import two functions from Node into our test file: `test` and `assert`: + +```js {title="timeConverter.test.js"} +import {formatAs12HourClock} from "./timeConverter.js"; +import assert from "node:assert"; +import test from "node:test"; +``` + +Now we have access to everything we need to get started. + + +### Defining a test + +We're going to use the `test()` function to define our test. Every time we use `test()` we need to pass it two arguments: +- A string describing what we're testing +- A function where we will call the function we are testing and define the expected outcome + +Passing a function into another function like this may look strange but is a very common pattern in JavaScript. We will look at it in more detail in a future module. + +We'll start by providing the string and an empty function. + +```js {title="timeConverter.test.js"} +import {formatAs12HourClock} from "./timeConverter.js"; +import assert from "node:assert"; +import test from "node:test"; + +test("correctly convert time after 12:00", function(){ + // TODO +}); +``` + +Inside the function we are going to use `assert` to compare two values: +- the _actual_ value returned when we call the function we are testing +- the _expected_ value we would see if everything is working correctly + +```js {title="timeConverter.test.js"} +import {formatAs12HourClock} from "./timeConverter.js"; +import assert from "node:assert"; +import test from "node:test"; + +test("correctly convert time after 12:00", function(){ + assert.equal(formatAs12HourClock("23:00"), "11:00 pm"); +}); +``` + +When we run our test: +1. The value `"23:00"` will be passed to `formatAs12HourClock` +2. The code in the function will be executed and the returned value will be passed to `assert.equal()` as its first argument, representing the _actual_ value +3. The _actual_ value will be compared to the second argument, representing the _expected_ value +4. The test will **pass** if the two values match. If they don't it will **fail**. + +Run the test from the terminal using `node timeConverter.test.js`. You should see the string we passed to `test()` printed in green with a check mark next to it, meaning our test passed! Success! \ No newline at end of file diff --git a/common-content/en/module/js1/testing/fizzbuzz/index.md b/common-content/en/module/js1/testing/fizzbuzz/index.md new file mode 100644 index 000000000..1664ca70e --- /dev/null +++ b/common-content/en/module/js1/testing/fizzbuzz/index.md @@ -0,0 +1,386 @@ ++++ +title = 'Test-Driven Development in Practice' + +time = 90 +[objectives] + 1='Follow the TDD workflow when writing code' +[build] + render = 'never' + list = 'local' + publishResources = false ++++ + +We've seen how to write tests, how to interpret the results and how to add testing libraries to our projects. Now it's time to pull it all together... + +### The problem + +We're going to look at a common coding problem which is based on a children's game called [fizz buzz](https://en.wikipedia.org/wiki/Fizz_buzz). In the game players sit in a circle and take it in turns to count up from the number 1. Certain numbers are replaced by the words "fizz" or "buzz" and if a player says their number instead of one of those words they are out of the game. + +The logic for this puzzle is fairly simple and as a result it has become a popular choice to assess candidates in technical interviews. We're going to use test-driven development to write a function which takes a number as an argument and returns the appropriate value as a string. + +There are many variations on the game but we're going to stick with the standard rules: +- If a number is divisible by 3 return "fizz": `3 --> "fizz"` +- If a number is divisible by 5 return "buzz": `5 --> "buzz"` +- If a number is divisible by 3 **and** 5 return "fizzbuzz": `15 --> "fizzbuzz"` +- If a number is divisible by neither 3 nor 5 return the number as a string: `7 --> "7"` + +For the purposes of this example we will assume that our inputs will all be numbers greater than 0. If we were doing this for real we should be checking that and writing tests to ensure we handle those cases too! + +### Setting up + +Before we start coding we need an environment to work in. + +{{}} +Create the necessary directory and files for this project. We'll need a file to write our function in (let's call it `fizzbuzz.js`) and a file for our tests. Don't forget to initialise a Git repository! +{{}} + +We have already reached our first major decision point: which testing framework should we use? + +As a general rule we don't want to add any more to our projects than we need to. We certainly don't need to consider things like front end simulation or database integrations here, so we don't need tools with that level of complexity. All we need to do is compare the output of a function to an expected value. We _could_ use jest, but that would mean configuring npm, adding packages and writing a script to run our tests. It will be much more straightforward, and ultimately more efficient, to use `node:test` in this case. + +### The first test + +Before writing any test in this exercise, think back to the diagram in the previous section: + +![red-green-refactor cycle](red-green-refactor.png) + +When we write our first test we should **watch it fail** before starting to work on the function. Let's start with the first case in our specification: division by 3. + + +{{}} +Most testing frameworks will let us organise our tests using a `describe` block. Their structure is similar to a test: their first argument is a string describing the block's content and the second is a function. We will use them to group related tests together like this: + +```js {title="fizzbuzz.test.js"} +describe('division by 3', () => { + + // Tests are defined here + +}); +``` +{{}} + +Our first test will check that `fizzbuzz(3)` will return `"fizz"`. Set it up as shown below: + +```js {title="fizzbuzz.test.js"} +import {fizzbuzz} from './fizzbuzz.js'; +import assert from 'node:assert'; +import {test, describe} from 'node:test'; + +describe('division by 3', () => { + + test('3 returns fizz', () => { + assert.equal(fizzbuzz(3), "fizz"); + }); + +}); +``` + +Running the test doesn't give us a "pass" or "fail" output though, it throws an error. + +{{}} +Let's put your debugging skills into practice! Read the error message and research what's causing it. + +
+Solution: + +The test can't find a function called `fizzbuzz` being exported from `fizzbuzz.js`. That shouldn't be a surprise though - we haven't written it yet! +
+{{
}} + +This step may seem pointless but it's still an important one to take. We may not learn anything new from watching this test fail, but if it passes at this stage then it tells us that we have made a mistake in setting up the test. + +Let's switch files and fix the problem. We don't want to go too far here, even though it can be tempting. Remember that when we are following TDD we write the tests such that they check our program does everything laid out in its specification. If something is in the spec there should be a test for it. That means that if our tests pass, our program does what it's supposed to do. If our tests fail they will tell us why, so we fix the problem. If we do any more than fix the problem in front of us we may end up writing more code than we need to, which may ultimately not have test coverage. + +All of that means that we should **only write enough code to fix the error**: + +```js {title="fizzbuzz.js"} +function fizzbuzz(){}; + +export {fizzbuzz}; +``` + +That's it! It feels strange stopping there but that's all the information we had to work with. We did fix the error though, and now we have a different one to guide our next step. We're getting an assertion error: our expected value is `"fizz"` but our actual value is `undefined`. It's another easy bug to fix, and just like before we'll do just enough to fix it. + +```js {title="fizzbuzz.js"} +function fizzbuzz(){ + return "fizz"; +}; + +export {fizzbuzz}; +``` + +Our test passes, and it's time to write some more. + +{{}} +We have a test and it's passing, so now would be an _excellent_ time to make a commit! Our commits should represent stable points we can roll back to if necessary, so committing when all our tests pass means that our code was working at that point in time. If we make a change and something goes wrong we know that everything will be fine if we revert to this commit. +{{}} + +Only testing a behaviour for one possible input is quite risky so we'll write some more tests for division by three. + +```js {title="fizzbuzz.test.js"} +describe('division by 3', () => { + + test('3 returns fizz', () => { + assert.equal(fizzbuzz(3), "fizz"); + }); + + test('6 returns fizz', () => { + assert.equal(fizzbuzz(6), "fizz"); + }); + + test('99 returns fizz', () => { + assert.equal(fizzbuzz(99), "fizz"); + }); + +}); +``` + +Three tests, three passes! Time to check out those other behaviours. + +### Testing the next requirement + +Our next bullet point is about division by five. We're testing a different behaviour now so we'll add another `describe` block to contain the tests. + +```js {title="fizzbuzz.test.js"} +describe('division by 5', () => { + + test('5 returns buzz', () => { + assert.equal(fizzbuzz(5), "buzz"); + }); + +}); +``` + +We have another assertion error, this time we expect `"buzz"` but our actual value is `"fizz"`. That shouldn't be a surprise - the only thing our function does is return `"fizz"`! + +{{}} +Update the `fizzbuzz` function so that the test passes. Remember that we only need to write enough code to make the test pass! + +
+Solution: + +The only thing we know for sure from our tests is that when `fizzbuzz()` receives `5` as an argument it should return `"buzz"`, so that's the **only** thing we will check for in the function. We don't have test coverage for anything else. + +We also uncover another problem: we haven't defined a parameter for the function! This wasn't a problem before because we were returning `"fizz"` for everything, but now we need to check the value passed to the function. This goes to show that even if we _think_ we have good test coverage we can still miss some fairly major issues. + +```js {title="fizzbuzz.js"} +function fizzbuzz(number){ + if (number === 5){ + return "buzz"; + } + return "fizz"; +}; +``` + +
+{{
}} + +Note that we still run all of our tests. We need to be sure we haven't introduced a bug anywhere else when making changes. + +It's time to write more tests for division by 5: + +```js {title="fizzbuzz.test.js"} +describe('division by 5', () => { + + test('5 returns buzz', () => { + assert.equal(fizzbuzz(5), "buzz"); + }); + + test('10 returns buzz', () => { + assert.equal(fizzbuzz(10), "buzz"); + }); + + test('95 returns buzz', () => { + assert.equal(fizzbuzz(95), "buzz"); + }); + +}); +``` + +We have failing tests again, with the same assertion error as before. + +This time our fix is a bit more complicated than it was when we added more tests for division by three. We _could_ add `else-if` clauses for each additional number we test but that wouldn't scale well at all. Instead we need to make our check more generic to account for _any_ number which is divisible by five. + +{{}} +Research how to check if one number is divisible by another and update the `if` statement to return `"buzz"` for any value divisible by five. + +
+Solution: + +```js {title="fizzbuzz.js"} +function fizzbuzz(number){ + if (number % 5 === 0){ + return "buzz"; + } + return "fizz"; +}; +``` + +
+{{
}} + + +### Refactoring to meet requirements + +Refactoring is an important part of the development lifecycle. Solving a problem is one thing, but solving it _well_ often needs us to make changes for efficiency. We need to consider our future selves too - we need to be able to understand what we wrote! + +Often we will be forced into a refactor by the discovery of a bug. These could be fairly small changes but they could also be pretty big. By following TDD we aim to catch as many of these while still in development and get most of our refactoring done as early as possible. + +{{}} +Create another `describe` block and write tests for the `"fizzbuzz"` output. Use `15`, `30` and `90` as the inputs. + +
+Solution: + +```js {title="fizzbuzz.test.js"} +describe('division by 3 and 5', () => { + + test('15 returns fizzbuzz', () => { + assert.equal(fizzbuzz(15), "fizzbuzz"); + }); + + test('30 returns fizzbuzz', () => { + assert.equal(fizzbuzz(30), "fizzbuzz"); + }); + + test('90 returns fizzbuzz', () => { + assert.equal(fizzbuzz(90), "fizzbuzz"); + }); + +}); +``` + +
+{{
}} + +Assertion errors again, this time expecting `"fizzbuzz"` and receiving `"buzz"`. No problem though, we've done this before. Let's add another clause to our `if` statement to handle this case. + +```js {title="fizzbuzz.js"} +function fizzbuzz(number){ + if (number % 5 === 0){ + return "buzz"; + } else if (number % 15 === 0){ + return "fizzbuzz"; + } + return "fizz"; +}; +``` + +Running our tests gives us a surprising result though: we have the same three failures with the same three assertion errors. It looks like the `fizzbuzz()` function is sending all of these inputs down the wrong branch of the `if` statement. + +At this point it would be useful to use VSCode's debugging tools to step through the code as the test runs and watch how each line is evaluated. That can be an incredibly useful tool when dealing with complex logic and complex function calls but in this situation we can already see what's going wrong. The question we have is _why_. Remember that you aren't limited to the tools in front of you when debugging, and in this case a bit of old-fashioned Googling will probably get us to an answer quicker than the debugger. + +The issue is a mathematical one: 15 is divisible by 5, so if a number is divisible by 15 then it is _also_ divisible by 5. Remember that an `if` statement stops once a condition is satisfied, so by checking division by 5 first we are also catching values which are divisible by 15 and sending them down the wrong path. + +{{}} +Update the `if` statement so that we check for divisibility by 15 before divisibility by 5. + +
+Solution: + +```js {title="fizzbuzz.js"} +function fizzbuzz(number){ + if (number % 15 === 0){ + return "fizzbuzz"; + } else if (number % 5 === 0){ + return "buzz"; + } + return "fizz"; +}; +``` +
+{{
}} + +Now our tests pass! We're very nearly there, we just have one more requirement to cover: returning the number as a string if not divisible by three or five. Let's write some tests: + +```js {title="fizzbuzz.test.js"} +describe('returning the number as a string', () => { + + test('1 returns "1"', () => { + assert.equal(fizzbuzz(1), "1"); + }); + + test('4 returns "4"', () => { + assert.equal(fizzbuzz(4), "4"); + }); + + test('91 returns "91"', () => { + assert.equal(fizzbuzz(91), "91"); + }); + +}); +``` + +We have three failing tests as expected and all three are failing with similar assertion errors: in each case the actual value is `"fizz"`. At the moment we're using this as a catch-all value if a number isn't divisible by fifteen or five, but really it should only be returned if the number is divisible by three. We need to update our logic again. + +We could easily pass these new tests by changing the final return statement: + +```js {title="fizzbuzz.js"} +function fizzbuzz(number){ + if (number % 15 === 0){ + return "fizzbuzz"; + } else if (number % 5 === 0){ + return "buzz"; + } + return number.toString(); +}; +``` + +This highlights the importance of running all our tests though, as our "division by three" tests are now failing. We fixed one problem but broke something else. We're going to need to add another clause to the `if` statement to get all our tests passing: + +```js {title="fizzbuzz.js"} +function fizzbuzz(number){ + if (number % 15 === 0){ + return "fizzbuzz"; + } else if (number % 5 === 0){ + return "buzz"; + } else if (number % 3 === 0){ + return "fizz"; + } + return number.toString(); +}; +``` + +### Refactoring for quality + +Remember what we said earlier: refactoring is an important step in writing good-quality code. At the moment we have a solution which works, but could it be better? + +We can start by looking at the conditions we are checking. The first clause may be technically correct, but our specification didn't say anything about checking for division by 15. Instead it spoke about division by 3 **and** by 5. Mathematically speaking it may be the same thing, but we can certainly make it clearer that this clause relates to that requirement. + +```js {title="fizzbuzz.js"} +function fizzbuzz(number){ + if (number % 3 === 0 && number % 5 === 0){ + return "fizzbuzz"; + } else if (number % 5 === 0){ + return "buzz"; + } else if (number % 3 === 0){ + return "fizz"; + } + return number.toString(); +}; +``` + +We can do something about the length of the function too. + +{{}} +[Guard clauses](https://blog.webdevsimplified.com/2020-01/guard-clauses/) are a useful tool to avoid overly-complex conditional statements. Read the linked article and use guard clauses to condense the logic in the function to four lines. + +
+Solution: + +```js {title="fizzbuzz.js"} +function fizzbuzz(number){ + if (number % 3 === 0 && number % 5 === 0) return "fizzbuzz"; + if (number % 5 === 0) return "buzz"; + if (number % 3 === 0) return "fizz"; + return number.toString(); +}; +``` +
+{{
}} + +### Summary + +This may feel like a lot of work for a small problem but it has already demonstrated some of the potential issues we can run into. Imagine, for example, that we hadn't written tests for the `"fizzbuzz"` cases and just assumed our code was correct. We may not have caught the bug until an actual user was interacting with it and by that point there are many more layers of infrastructure clouding the picture. + +By writing the tests first we can make sure that our program's requirements are captured and represented in a way that gives developers clear feedback if there is a problem with the code. Testing like this is an important skill and it is one we will reinforce throughout the rest of this course. \ No newline at end of file diff --git a/common-content/en/module/js1/testing/fizzbuzz/red-green-refactor.png b/common-content/en/module/js1/testing/fizzbuzz/red-green-refactor.png new file mode 100644 index 000000000..bc4b2dd67 Binary files /dev/null and b/common-content/en/module/js1/testing/fizzbuzz/red-green-refactor.png differ diff --git a/common-content/en/module/js1/testing/tdd-intro/index.md b/common-content/en/module/js1/testing/tdd-intro/index.md new file mode 100644 index 000000000..2040830c0 --- /dev/null +++ b/common-content/en/module/js1/testing/tdd-intro/index.md @@ -0,0 +1,53 @@ ++++ +title = 'Test-Driven Development' + +time = 20 +[objectives] + 1='Explain the benefits of test-driven development' +[build] + render = 'never' + list = 'local' + publishResources = false + ++++ + +So far we have been writing our tests after we have written our functions and using them to confirm that the functions do what they are supposed to do. The tests we write are still valid, but by taking this approach we risk [confirmation bias](https://thedecisionlab.com/biases/confirmation-bias) - we write the tests to prove something we already know to be true. + +We can avoid this by writing the tests _before_ we write any code. This process is called **test-driven development** (**TDD**). We write tests which cover the desired behaviour of our function, then write the code to make those tests pass. + +### How could TDD have helped last sprint? + +Think back to the last sprint and how we built up the test suite for `convertTo12HourClock`: +- We wrote a test to ensure it worked for an afternoon time (`"23:00"`) +- We wrote a test to ensure it worked for a morning time and discovered a bug (`"08:00"`) +- We realised we forgot an edge case (`"00:00"`) and had to modify the function again. + +After each step we thought we were done, but we weren't. We're still not finished now - we haven't written any tests to validate inputs, or checked early afternoon times. Because we were working in this file a lot while we learned about testing we found each of these issues quickly, but if we were working on a real-world project there could be a long time between "finishing" the code, discovering a missing test and fixing any bugs that arise. That's a lot of opportunities for something to go wrong. + +Instead our workflow could have been: +- Write tests for afternoon time, morning time and the midnight edge case +- Write our first attempt at the function body +- See some tests pass and some fail +- Immediately fix bugs or add missing logic + +We know what we need to do before we even start coding and we have the tools in place to identify problems before we declare ourselves finished. It doesn't guarantee that our code will be perfect, but it means many of the potential problems will be fixed before we declare ourselves "finished". + +### Red-Green-Refactor + +An important aspect of TDD is the need to verify that our code is what's making the test pass. That means ensuring that the test isn't passing by itself without us writing anything. If that happens we may have a poorly-defined test. + +Watching the tests fail first is part of the **red-green-refactor** cycle: + +![red green refactor cycle](red-green-refactor.png) + +- Write a test +- Run the test file and watch the test fail +- Write enough code to make the test pass - **no more than necessary!** +- Run the test again and make sure it passes +- Refactor the code if necessary to improve readability or efficiency +- Run the test again to make sure it still passes +- Repeat with the next test + +After modifying the function being tested we should always re-run _all_ of the tests, not just those for the feature we are writing. Making a change in one place can easily break something somewhere else. + +It can be difficult to get into the TDD mindset, but once we do there are real benefits to it. In the next section we'll look at an in-depth example of the TDD workflow. \ No newline at end of file diff --git a/common-content/en/module/js1/testing/tdd-intro/red-green-refactor.png b/common-content/en/module/js1/testing/tdd-intro/red-green-refactor.png new file mode 100644 index 000000000..bc4b2dd67 Binary files /dev/null and b/common-content/en/module/js1/testing/tdd-intro/red-green-refactor.png differ diff --git a/common-content/en/module/js1/testing/testing-intro/index.md b/common-content/en/module/js1/testing/testing-intro/index.md new file mode 100644 index 000000000..8682dafad --- /dev/null +++ b/common-content/en/module/js1/testing/testing-intro/index.md @@ -0,0 +1,85 @@ ++++ +title = 'Introduction to Testing' +time = 30 +hide_from_overview = true +[objectives] + 1='Explain the need for testing when writing code' + 2='Define "unit testing"' +[build] + render = 'never' + list = 'local' + publishResources = false + ++++ + +In the last module we introduced the idea of writing functions to help us reuse blocks of code. We can build a complete application using these blocks, and even incorporate blocks built by other people. That's a lot of things which all have to work together correctly - how can we make sure that happens? + +### Testing + +We make sure by **testing** our code! Testing doesn't have any special meaning in software - we are going to check that a program does the right thing at the right time. There are many different ways for us to do that though. + +In this module we are going to concentrate on **unit testing**. That means we are testing the individual components of a program - our functions - to ensure they work correctly. In a real project we would also consider how these components work together and with other systems (known as **integration testing**). + +Testing code is something that every developer should be doing, but it wouldn't make much sense for every developer to write their own tools to test their code. We're going to use a package to help us write our tests. + +### Our function + +Before we start writing tests we're going to write the function which we will be testing. In this example we're going to create a function which will take a time in 24-hour format (eg. `15:00`) and convert it to 12-hour format (`3:00 pm`). We will name our function `formatAs12HourClock`. Create a new directory to store your files and a new `timeConverter.js` file. + +Stating our problem in the given-when-then structure: + +- _Given_ a time in 24-hour format +- _When_ we call `formatAs12HourClock` +- _Then_ we get back a string representing the same time in 12-hour + +To do the conversion we will need to examine the input and determine if the part of it representing the hour is over or under 12. If it's under we don't need to change it, if it's over we need to subtract 12 to get the 12-hour equivalent. Finally we need to add `am` or `pm` and return the new value. + +Converting that to pseudocode: + +```js {title="timeConverter.js"} +// function receives a string representing time in 24-hour format as an argument +// extract digits representing hours +// if hour value over 12, subtract 12 +// if hour value under 12, continue +// add am or pm +// return new value +``` + +We can write our function as: + +```js {title="timeConverter.js"} +// function receives a string representing time in 24-hour format as an argument +function formatAs12HourClock(time) { + + // extract digits representing hours + const hours = Number(time.slice(0, 2)); + + // if hour value over 12, subtract 12 + // if hour value under 12, continue + if (hours > 12) { + // add pm and return value + return `${hours - 12}:00 pm`; + } + // add am and return value + return `${hours} am`; +} +``` + +{{}} +There are two functions used here which you may not have seen before: +- `Number()` +- `String.slice()` + +Use the [MDN docs](https://developer.mozilla.org/en-US/) to research these functions and understand what they are doing here. +{{}} + +We can check that our function works by calling it a couple of times and using `console.log()` to print the results. + +```js {title="timeConverter.js"} +// ... + +console.log(formatAs12HourClock("23:00")); +console.log(formatAs12HourClock("14:00")); +``` + +This does the job, but it doesn't scale well at all. Imagine we have a lot of functions to test - that would mean lots of `console.log()` calls cluttering up our files. It also relies on people running the file using Node, so if our tests require anything more complex like a database integration it won't be possible to run them properly. We're going to move our tests into an environment where it's much easier to keep track of everything. diff --git a/common-content/en/module/js1/testing/what-to-test/index.md b/common-content/en/module/js1/testing/what-to-test/index.md new file mode 100644 index 000000000..398c3c38b --- /dev/null +++ b/common-content/en/module/js1/testing/what-to-test/index.md @@ -0,0 +1,76 @@ ++++ +title = 'Knowing What to Test' + +time = 15 +[objectives] + 1="Identify gaps in test coverage" + 2="Define an edge case" +[build] + render = 'never' + list = 'local' + publishResources = false + ++++ + +We have multiple tests, so _now_ can we confidently say that our function works? + +The answer is still no! We need to be sure that our function does everything that the specification says it should, but we also need to think about how it handles unusual inputs or internal errors in the logic. We often need to write lots of tests for each function to be sure they won't break. It's a lot of work, but the payoff is reliable code which we can be sure won't fail in production. + +### What do we still need to test? + +It can be difficult to know when we're done writing tests, but at a minimum you should be able to cover every scenario covered by the requirements of your project. Another approach is to ask yourself a series of "what if?" questions and see if your tests cover that scenario. In our case we might ask "What if... + +- ...the expected value is a single-digit afternoon time, eg `"02:00 pm"`? +- ...the argument is not a valid time, eg `"25:00"`? +- ...the argument isn't a time at all, eg. `"hello"`? +- ...the argument isn't a string? +- ...and many more questions like these + +The `assert` library has other functions available to support tests like these, eg. `assert.throws()` checks that an error is thrown by a function at an appropriate time. As your applications get more complex you will likely need to bring in external tools to test specific elements of your code, eg. simulating a button being clicked in a web browser. We will look at how we can add additional testing tools in the next sprint. + +An application's **test coverage** gives us an indication of how many of the functions in a program have been tested and how extensively. More test coverage is always better! + +### Edge cases + +A lot of the strange behaviour we see from programs is cause by a small subset of possible inputs. Think about our time conversion function: how should it handle `"00:00"`? As a human reader we know that this should be converted to `"12:00 am"`, but the logic we have written would convert it to `"00:00 am"`. We need to think about how we handle this special case. + +This is an example of an **edge case**, where we have a possible value which needs special consideration. Often these values don't need any adjustments to the code, but in others (like this one) we need to make changes to ensure they are handled correctly. Many of the tests you write will be designed to handle these edge cases. + +{{}} +Update `formatAs12HourClock` to handle this edge case and write a test to ensure it does. + +
+Solution: + +```js {title="timeConverter.js"} +function formatAs12HourClock(time) { + const hours = Number(time.slice(0, 2)); + + // This is not the only way to complete this check. + // If you did it a different way why not share your solution in Slack? + if (time === "00:00"){ + return `12:00 am`; + } + + if (hours > 12) { + return `${hours - 12}:00 pm`; + } + + return `${time} am`; +} + +export {formatAs12HourClock} +``` + +```js {title="timeConverter.test.js"} +//... +test("can correctly convert midnight", function(){ + assert.equal(formatAs12HourClock("00:00"),"12:00 am"); +}); +``` + +
+ +{{
}} + +If a function has multiple inputs then it's possible that two or more of these could represent edge cases. We call these scenarios **corner cases** - multiple edges are meeting each other. \ No newline at end of file diff --git a/common-content/en/module/package-management/npm/index.md b/common-content/en/module/package-management/npm/index.md new file mode 100644 index 000000000..37a950926 --- /dev/null +++ b/common-content/en/module/package-management/npm/index.md @@ -0,0 +1,101 @@ ++++ +title = 'Package Management in JavaScript' +time = 20 +[objectives] + 1="Define a package" + 2="Define a dependency" + 3="Explain the purpose of `node_modules`" +[build] + render = 'never' + list = 'local' + publishResources = false ++++ + +In the last module we started to write reusable blocks of code by defining [functions](itp/javascript-fundamentals/sprints/3/prep/#functions). Using functions helps to keep our code clean and maintainable, and as an added bonus we only need to write the logic out once! We're not the only developers doing this though - _everyone_ is trying to reuse code wherever they can. + +This practice is an established part of a typical workflow and every language has its own tools to support this. In this section we will look at how we can set up one of the JavaScript tools to support our development. + +### Setting up a package manager. + +When we bundle code together and share it we publish it as a **package**. In order to use someone else's code in our projects we need to use a **package manager** to install it. The package manager we will use is called **npm**. + +{{}} +npm is not the only package manager available for JavaScript, **yarn** is a popular alternative. If you have experience in other languages you may have used package managers there, eg. Python users may have used pip. Each tool has its own commands and ecosystem but the core concepts are the same. +{{}} + +Before we start using npm in a project we need to so some setup. It was already installed for us when we set up Node but we also need to configure the project. Create a new directory called `packages-practice` and navigate there in your terminal. Once you are there use the command `npm init -y` to start the setup. + +```sh {title="username/cyf-work/packages-practice"} +npm init -y +``` + +You should see some output printed: + +```console +Wrote to username/cyf-work/packages-practice/package.json: + +{ + "name": "packages-practice", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs" +} +``` + +Look carefully at the first line: it says something was written to a file. If we check using `ls` we'll see that there's now a file called `package.json`, and if we open the directory in VSCode we see that it includes all the information printed above. Now we have this file we can use npm to do a few different things with our project, but for now we'll focus on adding packages. + +{{}} +This file is written in **JSON** - **J**ava**S**cript **O**bject **N**otation. Values before a colon are **keys** and the values after the colons are the associated **values**. Using this structure we can quickly find important information about our project. + +We will look at JavaScript objects in more detail in the next module. +{{}} + +{{}} +In this example we added `-y` to the end of the setup command. This was optional, but by including it we prep-populated `package.json` with some common default values. If you don't include the flag the command will still work but you will be prompted to add a value for each property before the file is created. +{{}} + +### Installing a package + +We're going to add our first package. We're going to use [is-odd](https://www.npmjs.com/package/is-odd) which provides logic to check if a number is odd or not. This is a very simple example of the workflow, but the process would be the same if we were adding a more complex package. + +{{}} +The link above leads to [www.npmjs.com](https://www.npmjs.com). This site has a searchable list of packages available to install through npm - if you're looking for something specific you should start here! +{{}} + +Switch back to your terminal and make sure you are in the same directory as `package.json`, then type the command below: + +```sh +npm install --save is-odd +``` + +Let's break down the command: + +- `npm` indicates that the command is run using npm +- `install` indicates that we want to install a package +- the `--save` flag adds additional instructions about _how_ we save the package. We will see some alternatives later in this sprint. +- `is-odd` is the name of the package we want to install + +Switch back to VSCode and you will see some new information at the bottom of `package.json`: + +```json {title="package.json"} +{ + // ... + "dependencies": { + "is-odd": "^3.0.1" + } +} +``` + +The `is-odd` package is now listed in our project as a **dependency**. This is important information for anyone else who wants to run our project: it tells them that our code **depends** on something from `is-odd` and they will need to install it too. + +Take a look in the file explorer tab and you will see there is also now a folder called `node_modules`. If you open it up you will see a directory for our `is-odd` package which contains the code it needs to run. When we ran `npm install` this is what was downloaded. There is also a directory for something else called `is-number`, which is a dependency of `is-odd`. It's very common for additional packages to be installed to support the one we need. + +Think back to the last sprint where we spoke about [`.gitignore` files](itp/javascript-fundamentals/sprints/3/prep/#ignoring-files). In that section we saw a `.gitignore` with `node_modules` already included in it, and now we can start to see why. If we tried to track everything in `node_modules` with Git we would end up with a very bloated repository and the potential for _lots_ of conflicts. Instead we ignore the folder and ask anyone using our code to download their own copy of the packages. + diff --git a/common-content/en/module/package-management/using-a-package/index.md b/common-content/en/module/package-management/using-a-package/index.md new file mode 100644 index 000000000..1b25b2763 --- /dev/null +++ b/common-content/en/module/package-management/using-a-package/index.md @@ -0,0 +1,64 @@ ++++ +title = 'Using a Package' +time = 30 +[objectives] + 1="Use `import` to include a package in a project" + 2="Use third-party code to add functionality to a project" +[build] + render = 'never' + list = 'local' + publishResources = false ++++ + +We have added a package to our project, now it's time to use it. + +### `import`ing the package + +Before we can use the package we need a file to work in. Create a new file called `checkingOddNumbers.js` in your `packages-practice` directory. + +We also need to make a change to the `package.json` file. The way we load the package into our code depends on how the project is configured, so we need to update the `type` property on line 12. Change its value to `module` as shown below: + +```json {title="package.json"} +{ + // ... + "type": "module", + // ... +} +``` + +Now we can hook up our package. At the top of `checkingOddNumbers.js` we need to add an `import` statement. Any time we need to use code which is defined in a different file we need to import it. + +```js {title="checkingOddNumbers.js"} +import isOdd from 'is-odd'; +``` + +Generally we will specify which functions we want to import (`isOdd` in this case) to avoid bloating our program too much. When importing from a package we only need to provide the name of the package in quotes, when importing from another file in our project we need to give its relative path. + +### Using the package + +Once we have imported the code we can use it just like any other function we defined ourselves. Try it by calling it a couple of times and printing the values. + +```js {title="checkingOddNumbers.js"} +import isOdd from 'is-odd'; + +console.log(isOdd(1)); +// true + +console.log(isOdd(2)); +// false +``` + +In the rest of this sprint we will be following a similar workflow: add a package using npm; import it into our files; use the functions it provides. + +{{}} +Try to recreate the workflow for yourself. + +1. Create a new directory called `translating-five` +2. Initialise an npm project there - you can use the default values +3. Install the [five](https://www.npmjs.com/package/five) package. It does fun things with the number 5 +4. Create a new file to work in and import the package +5. Use the documentation on npmjs to help you translate "five" into the following languages. You should print `Five in is `: + - Dutch + - Japanese + - Binary +{{}} \ No newline at end of file diff --git a/org-cyf/content/itp/testing/sprints/1/_index.md b/org-cyf/content/itp/testing/sprints/1/_index.md index 60fee00d9..93d81d7fb 100644 --- a/org-cyf/content/itp/testing/sprints/1/_index.md +++ b/org-cyf/content/itp/testing/sprints/1/_index.md @@ -4,5 +4,5 @@ description = 'The plan for this sprint' layout = 'sprint' menu_level = ['module'] weight = 1 -theme = "Programming fundamentals" +theme = "Testing our code" +++ diff --git a/org-cyf/content/itp/testing/sprints/1/prep/index.md b/org-cyf/content/itp/testing/sprints/1/prep/index.md index 5bdaafbfb..bdf5a58ab 100644 --- a/org-cyf/content/itp/testing/sprints/1/prep/index.md +++ b/org-cyf/content/itp/testing/sprints/1/prep/index.md @@ -1,10 +1,27 @@ +++ -title = 'prep' -description = '**Prerequisites:** a terminal, basic arithmetic.' +title = 'Prep' layout = 'prep' menu_level = ['sprint'] weight = 1 [[blocks]] name="Playing computer" src="module/js1/playing-computer" +[[blocks]] +src="module/js1/testing/testing-intro" +name="Intro to testing" +[[blocks]] +src="module/js1/testing/first-test" +name="First test case" +[[blocks]] +src="module/js1/testing/failing-tests" +name="Failing tests" +[[blocks]] +src="module/js1/testing/what-to-test" +name="What to test?" +[[blocks]] +src="module/js1/anonymous-functions" +name="Anonymous functions" +[[blocks]] +src="module/js1/arrow-functions" +name="Arrow functions" +++ diff --git a/org-cyf/content/itp/testing/sprints/2/_index.md b/org-cyf/content/itp/testing/sprints/2/_index.md index 97c5203e0..f56eeb069 100644 --- a/org-cyf/content/itp/testing/sprints/2/_index.md +++ b/org-cyf/content/itp/testing/sprints/2/_index.md @@ -4,5 +4,5 @@ description = 'The plan for this sprint' layout = 'sprint' menu_level = ['module'] weight = 2 -theme = "Comparisons, assertions, and breaking down problems" +theme = "Package management; Testing methodologies" +++ diff --git a/org-cyf/content/itp/testing/sprints/2/prep/index.md b/org-cyf/content/itp/testing/sprints/2/prep/index.md index a6087453a..6e609bb81 100644 --- a/org-cyf/content/itp/testing/sprints/2/prep/index.md +++ b/org-cyf/content/itp/testing/sprints/2/prep/index.md @@ -1,30 +1,24 @@ +++ -title = 'prep' +title = 'Prep' layout = 'prep' menu_level = ['sprint'] weight = 1 [[blocks]] -src="module/js1/clocks" -name="Clocks" +name="Package management" +src="module/package-management/npm/index.md" [[blocks]] -src="module/js1/assertions" -name="Assertions" +name="Using a package" +src="module/package-management/using-a-package/index.md" [[blocks]] -src="module/js1/sub-goal" -name="Sub-goal" +src="module/js1/jest/installing" +name="Testing libraries" [[blocks]] -src="module/js1/strings" -name="Strings" +src="module/js1/jest/cases" +name="First Jest test case" [[blocks]] -src="module/js1/testing-a-sub-goal" -name="Testing a sub-goal" +src="module/js1/testing/tdd-intro" +name="TDD" [[blocks]] -src="module/js1/sub-goal-2" -name="Sub-goal #2" -[[blocks]] -src="module/js1/refactoring-repetition" -name="Refactoring repetition" -[[blocks]] -src="module/js1/identifying-missing-tests" -name="Identifying missing tests" +src="module/js1/testing/fizzbuzz" +name="TDD in practice" +++ diff --git a/org-cyf/content/itp/testing/sprints/3/_index.md b/org-cyf/content/itp/testing/sprints/3/_index.md index ac74cc086..22f7967cf 100644 --- a/org-cyf/content/itp/testing/sprints/3/_index.md +++ b/org-cyf/content/itp/testing/sprints/3/_index.md @@ -4,5 +4,5 @@ description = 'The plan for this sprint' layout = 'sprint' menu_level = ['module'] weight = 3 -theme = "Test cases with Jest" +theme = "Working with Git in the terminal" +++ diff --git a/org-cyf/content/itp/testing/sprints/3/prep/index.md b/org-cyf/content/itp/testing/sprints/3/prep/index.md index 8dd52ba4f..2b1d53da8 100644 --- a/org-cyf/content/itp/testing/sprints/3/prep/index.md +++ b/org-cyf/content/itp/testing/sprints/3/prep/index.md @@ -1,45 +1,24 @@ +++ -title = 'prep' +title = 'Prep' layout = 'prep' menu_level = ['sprint'] weight = 1 [[blocks]] -src="module/js1/ordinal" -name="Ordinal numbers" -[[blocks]] -src="module/js1/framework" -name="Testing frameworks" -[[blocks]] -src="module/js1/setup" -name="Starting a project" -[[blocks]] -src="module/js1/packages" -name="Using packages" -[[blocks]] -src="module/js1/installing" -name="Installing Jest" -[[blocks]] -src="module/js1/api" -name="Jest's API" -[[blocks]] -src="module/js1/cases" -name="First test case" -[[blocks]] -src="module/js1/feedback" -name="Interpreting feedback" -[[blocks]] name="Dead Code" src="module/js1/dead-code" [[blocks]] -src="module/js1/generalise" -name="Generalising further" +name="Git in the terminal" +src="module/git-cli/initialisation" +[[blocks]] +name="Adding & committing" +src="module/git-cli/adding-committing" [[blocks]] -src="module/js1/anonymous-functions" -name="Anonymous functions" +name="Remote repositories" +src="module/git-cli/remote-repositories" [[blocks]] -src="module/js1/arrow-functions" -name="Arrow functions" +name="Pushing & pulling" +src="module/git-cli/pushing-pulling" [[blocks]] -name="Solving problems while testing πŸ“Ό" -src="module/js1/testing-workshop" +name="Branching & merging" +src="module/git-cli/branches" +++