📖 Table of contents
  1. Sprint 3
    1. Sprint assesment
    2. Retrospective
    3. 🏃‍♂️ Sprint 3 planning
    4. Testing
      1. Unit tests
      2. Integration tests
      3. UI tests
    5. Configuration for tests
    6. Testing REST API endpoints
    7. ⭐ Bonus: Test coverage
    8. Authentication
    9. Peer review
    10. Final report

Sprint 3

For the final Sprint of the course, the Sprint 3, we have a new set of requirements from the Product Owner. On top of working on new user stories, we will be covering topics related to testing and authentication.

Sprint assesment

This Sprint doesn’t have a Moodle submission. It is enough that everything mentioned in the exercises is pushed to the project’s GitHub repository before the Sprint deadline on 20.5. at 11:00. We will be working on the exercises for a bit over a week.

The Sprint assesment is done based on the exercises 1-27. The team can earn up to 10 points from this Sprint. This is the final Sprint of the course and the team’s project points will be composed of the points from this Sprint and the two previous Sprints. That is, the maximum number of project points is 30.

During this Sprint, each team member will write a peer review in which they asses themselves and other team members. The results of the peer review will heavily impact the personal points of a team member. Each team member can earn up to 10 personal points.

At the end of this Sprint, each team member has to submit the final report and the peer review. Both of them are required to pass the course. Submitting either of them after the Sprint deadline will decrease the personal points.

Retrospective

Organize a similar Mad, Sad, Glad retrospective in Flinga for the Sprint 2 as we did at the end of the Sprint 1.

Exercise 1

The Scrum Master should create a new session in Flinga as instructed above. Name the session “Retrospective 2”. Once the session is created, other team members should join the session with the “Join link”. Setup the session board and organize the Retrospective event.

Did similar issues arise as in Sprint 1 retrospective? If so, try to come up with different actions as before or ask the teacher for tips on how to solve these issues.

Once you have completed the Retrospective, add a link to the Retrospective’s Flinga board and write down the successes, issues and actions you came up during the Retrospective to the repository’s retrospectives/sprint2.md file and push the changes to GitHub.

Exercise 2

Choose a new (not the same team member as during the previous Sprint) Scrum Master among the team members for the third Sprint.

🏃‍♂️ Sprint 3 planning

If you weren’t able to implement all the user stories during the previous Sprint, start by finishing those before starting to implement the user stories for this Sprint.

The Product Owner was delighted to see how the project has advancend during Sprint 2.

The Sprint Review gave the Product Owner many new ideas on how to improve the application. Here’s how the Product Owner is describing the Sprint 3 goals in the Sprint Planning event:

“We now have the basic features for managing and taking quizzes. What we still need is a way for the teachers to manage their personal quizzes.

The teacher should be able to register with a username, email, bio and password in the teacher dashboard. The teacher should not be able to register with a username less than three characters long or a password less than eight characters long. The email should resemble a valid email address. The username or email should not be already taken by another registered user. The teacher can use the bio to describe themselves. The bio is optional but shouldn’t be more than 160 characters long. The user should also be required to retype the password to make sure that they didn’t accidently mistype the password.

Once registered, the teacher should be able to sign in with their username and password. If the user is not signed in the navigation bar should have “Register” and “Sign in” links, which will take the user to the register or sign in page.

An anonymous user, that is an user who is not signed in, should be able to see the quiz and category list. However, they should not be able to add a quiz or a category. That is, the links for adding a quiz and category should not be visible if the user is not signed in.

After signing in, the teacher should be able to add a quiz or a category. However, the teacher should only be able to edit and delete quizzes and categories they have added themselves. That is, the “Edit” link and the “Delete” button in the quiz list should only be visible if the teacher has added the quiz. The same logic should be applied to the categories and quiz’s questions. The quiz list should also display the username of the teacher who has added the quiz both in teacher and student dashboard.

The student should be able to share their thoughts about a quiz by writing a review. For this purpose there could be a separate review page. A review has a reviewer’s nickname, a rating between 1 and 5 and a review text. The student should not be able to add a review with a nickname less than three characters long, a blank review text or without a rating between 1 and 5. The student should not be able to review a non-published quiz.

The review page should list the added reviews from newest to oldest order. Each review should display the information submitted by the student and the date when the review was written. The review page should also display the review summary at the top of the page. The review summary should include the number of reviews the quiz has and the rating average. It should also be possible for the student to edit and delete reviews in the review page.”

– The Product Owner

After some discussion the Scrum Team planned the following user stories:

  1. As a teacher, I want to register an account so that I can manage my personal quizzes
  2. As a teacher, I want to sign in so that I can manage my personal quizzes
  3. As a teacher, I want to associate the added quizzes and categories with my account so that I can manage my personal quizzes
  4. As a student, I want to review a quiz so that I can share my thoughts about the quiz with others
  5. As a student, I want to see the reviews of a quiz so that I can learn what others think about the quiz
  6. As a student, I want to delete a review so that I can get rid of reviews I don’t need
  7. As a student, I want to edit a review so that I can change its information

Exercise 3

Create a new milestone for the third Sprint. Set the milestone title as “Sprint 3”.

Exercise 4

Make sure that all task related issues that have been completed during the Sprint 2 are closed and their status is “Done” in the Backlog project. Do the same with the user story related issues accepted by the Product Owner during the Sprint Review event.

If you didn’t manage to implement all user stories during the previous Sprint, set the milestone of the unfinished user story and task issues as “Sprint 3”. If the Sprint Review brought up implementation improvements or flaws (e.g. bugs), create appropriate issues for the tasks.

Exercise 5

Create an issue for each user story. Add the “user story” label for each issue. Set the milestone as “Sprint 3”. Add the issues to the Backlog project and move them to the “Sprint Backlog” column

The Authentication section covers topics related to the authentication. Take a look at it before planning the authentication-related tasks.

Exercise 6

Plan the tasks for the first user story, “As a teacher, I want to register an account so that I can manage my personal quizzes”. Read the Product Owner’s Sprint Planning description regarding the user story again and split it into small coding tasks.

Create an issue for each task. Set the milestone as “Sprint 3”. Add the issues to the Backlog project’s “Sprint Backlog” column.

The Scrum Team’s UI Designer’s vision is that the implementation could look something like this:

Exercise 7

Plan the tasks for the second user story, “As a teacher, I want to sign in so that I can manage my personal quizzes”. Read the Product Owner’s Sprint Planning description regarding the user story again and split it into small coding tasks.

Create an issue for each task. Set the milestone as “Sprint 3”. Add the issues to the Backlog project’s “Sprint Backlog” column.

The Scrum Team’s UI Designer’s vision is that the implementation could look something like this:

Exercise 8

Plan the tasks for the third user story, “As a teacher, I want to associate the added quizzes and categories with my account so that I can manage my personal quizzes”. Read the Product Owner’s Sprint Planning description regarding the user story again and split it into small coding tasks.

Create an issue for each task. Set the milestone as “Sprint 3”. Add the issues to the Backlog project’s “Sprint Backlog” column.

The Scrum Team’s UI Designer’s vision is that the implementation could look something like this:

Exercise 9

Plan the tasks for the fourth user story, “As a student, I want to review a quiz so that I can share my thoughts about the quiz with others”. Read the Product Owner’s Sprint Planning description regarding the user story again and split it into small coding tasks.

Create an issue for each task. Set the milestone as “Sprint 3”. Add the issues to the Backlog project’s “Sprint Backlog” column.

The Scrum Team’s UI Designer’s vision is that the implementation could look something like this:

Exercise 10

Plan the tasks for the fifth user story, “As a student, I want to see the reviews of a quiz so that I can learn what others think about the quiz”. Read the Product Owner’s Sprint Planning description regarding the user story again and split it into small coding tasks.

Create an issue for each task. Set the milestone as “Sprint 3”. Add the issues to the Backlog project’s “Sprint Backlog” column.

The Scrum Team’s UI Designer’s vision is that the implementation could look something like this:

Exercise 11

Plan the tasks for the sixth user story, “As a student, I want to delete a review so that I can get rid of reviews I don’t need”. Read the Product Owner’s Sprint Planning description regarding the user story again and split it into small coding tasks.

Create an issue for each task. Set the milestone as “Sprint 3”. Add the issues to the Backlog project’s “Sprint Backlog” column.

Exercise 12

Plan the tasks for the seventh user story, “As a student, I want to edit a review so that I can change its information”. Read the Product Owner’s Sprint Planning description regarding the user story again and split it into small coding tasks.

Create an issue for each task. Set the milestone as “Sprint 3”. Add the issues to the Backlog project’s “Sprint Backlog” column.

Testing

When we implement a new feature for the application we need to make sure that it works as intended. That is, we test the implementation of a feature against the requirements. During the development of a feature we are constantly performing manual testing for the implementation, meaning that we use the application ourselfs and see that we can perform certain actions successfully.

Manual testing is important, but we can’t perform it at scale. When our application becomes more complex, each change to code can potentially break any part of the application. Testing each feature manually after each change to code would be tome time-consuming. That’s why we implement automated tests: programs that test our code. We can usually execute hundreds of automated tests within just a minute. Martin Fowler explains the purpose of different kind of automated tests and their pros and cons in his article TestPyramid.

TestPyramid

Fowler categorizes different tests in three categories: unit, service and UI (user interface) tests. The test pyramid represents the amount of these different kind of tests we should have for our application. There are pros and cons for the different kind of tests. While we go up in the pyramid we get better reliability that our application works as inteaded as a whole, but the tests becomes laborious to maintain, difficult to implement, and time consuming to run. This is why our test portfolio should be balanced.

Automated tests are implemented with programming language specific testing frameworks, such as Java’s JUnit and JavaScript’s Vitest. During this Sprint we will implement some integration tests for our backend’s REST API endpoints using JUnit and the MockMVC framework.

Unit tests

Unit tests constitute the bottom of the test pyramid. Most of our application’s tests should be unit tests. Unit tests test the smallest testable parts of an application, called units. These are commonly simple methods that does some operation based on their parameters and return some value. Units never integrate to other parts of the application code, such as the database.

Here’s an example of unit tests for a calculateWords method, which returns the number of words in the string provided as the paratamer:

@Test
void calculateWordsCalculatesSingleWordCorrectly() {
    String message = "Hello";
    assertEquals(1, MessageUtils.calculateWords(message));
}

@Test
void calculateWordsCalculatesManyWordsCorrectly() {
    String message = "Hello world";
    assertEquals(2, MessageUtils.calculateWords(message));
}

@Test
void calculateWordsCalculatesZeroWordCorrectly() {
    String message = "";
    assertEquals(0, MessageUtils.calculateWords(message));
}

Unit tests have these pros and cons:

  • 🟢 Simple to implement and easy to maintain
  • 🟢 Fast to run
  • 🔴 Doesn’t provide good reliability that the application works as a whole

Integration tests

Integration tests (also known as service tests) constitute the middle of the test pyramid. We should have quite many integration tests for our application. As the name suggests, integration tests test that different parts of our application code work as inteded once they are integrated. For example, methods that perform database operations are tested with integration tests.

Here’s an example of integration tests for a REST API endpoint /api/messages implemented by the getAllMessages method:

@Test
public void getAllMessagesReturnsEmptyListWhenNoMessagesExist() throws Exception {
    this.mockMvc.perform(get("/api/messages"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$", hasSize(0)));
}

@Test
public void getAllMessagesReturnsListOfMessagesWhenMessagesExist() throws Exception {
    Message firstMessage = new Message("First message");
    Message secondMessage = new Message("Second message");
    messageRepository.saveAll(List.of(firstMessage, secondMessage));

    this.mockMvc.perform(get("/api/messages"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$", hasSize(2)))
            .andExpect(jsonPath("$[0].content").value("First message"))
            .andExpect(jsonPath("$[0].id").value(firstMessage.getId()))
            .andExpect(jsonPath("$[1].content").value("Second message"))
            .andExpect(jsonPath("$[1].id").value(secondMessage.getId()));
}

These tests send a request to the REST API endpoint and verify that the JSON response body contains the required information.

Integration tests have these pros and cons:

  • 🟢 Fairly simple to implement and easy to maintain
  • 🟢 Fairly fast to run
  • 🟡 Provides a reasonable reliability that the application works as a whole

UI tests

UI tests (also known as end-to-end tests) constitute the top of the test pyramid. We should have a moderate amount of UI tests in our application. As the name suggests, UI tests test that the application works by actually performing actions on the user interface similarly as a real user. This means opening a page on a web browser, filling form fields, clicking buttons and expecting the page to have some content. Because UI tests need the user interface to operate on, they are slow to execute. In addition, because the application’s user interface commonly changes more often than the code, UI tests are laborious to maintain.

Here’s an example of testing the submission of the message form with the Robot Framework test automation framework:

*** Test Cases ***
Submit Valid Message
    Go To http://localhost:8080/add/message
    Input Text content "Hello world!"
    Click Button "Add"
    Go To http://localhost:8080
    Page Should Contain "Hello world!"

UI tests have these pros and cons:

  • 🟢 Provides a good reliability that the application works as a whole
  • 🔴 Difficult to implement and laborious to maintain
  • 🔴 Slow to run

Configuration for tests

Because our tests will alter the database we should consider using a different database for tests. This is a common practice because we don’t want the tests to alter (for example delete) any data we are using during the development.

The database related configuration is in the src/main/resources/application.properties configuration file:

spring.datasource.url=jdbc:h2:file:~/quizzer;DB_CLOSE_ON_EXIT=FALSE;AUTO_RECONNECT=TRUE

The database is stored in a file located in ~/quizzer. We can use a different, in-memory database for the tests. To achieve this, we can add a test-specific src/test/resources/application.properties configuration file (note the test folder in the path):

spring.datasource.url=jdbc:h2:mem:quizzer-test;DB_CLOSE_ON_EXIT=FALSE;AUTO_RECONNECT=TRUE

The configuration in the src/test/resources/application.properties file will be used while we are running the tests, which makes it suitable for test-specific configuration.

Exercise 13

Add a test-specific configuration file and configure a separate database for the tests. Make sure that running the tests doesn’t alter (for example delete any data) the development environment database.

Testing REST API endpoints

“Write tests. Not too many. Mostly integration.”

– Kent C. Dodds

Integration tests are a great balance of reliability and performance. Kent C. Dodds covers the importance of integration tests in his article Write tests. Not too many. Mostly integration. As the name of the article implies, Dodds suggests that most of the tests for the application should be integration tests. He makes some fair points to justify this claim:

One thing that it doesn’t show though is that as you move up the pyramid, the confidence quotient of each form of testing increases. You get more bang for your buck. So while E2E tests may be slower and more expensive than unit tests, they bring you much more confidence that your application is working as intended.

To get some confidence that our application is working as inteded, let’s implement some integration tests for our REST API endpoints.

In Java applications, tests are implemented and executed with the JUnit testing framework. JUnit tests are implemented as test classes. Test classes can be annoted with the @SpringBootTest annotation to access the Spring application context in tests. This, for example makes the @Autowired annotations work. Methods annotated with the @Test annotation are the test methods, which test a specific test scenario.

Test methods usually share certain common setup code, which should be executed before each test method. This setup can be put inside a method annotated with the @BeforeEach annotation. The tests should be independent from each other, meaning that for example the order in which the tests are executed should not matter. To achieve the independence, each test needs to start with an empty database. This is achieved by deleting all entities in the setUp method before each test.

As an example, let’s consider testing the following methods of a MessageRestController class:

@RestController
@RequestMapping("/api/messages")
@CrossOrigin(origins = "*")
public class MessageRestController {
    @Autowired
    private MessageRepository messageRepository;

    @GetMapping("")
    public List<Message> getAllMessages() {
        return messageRepository.findAll();
    }

    @GetMapping("/{id}")
    public Message getMessageById(@PathVariable Long id) {
        return messageRepository.findById(id).orElseThrow(
            () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Message with id " + id + " does not exist"));
    }

    @PostMapping("")
    public Message createMessage(@Valid @RequestBody CreateMessageDto message, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                bindingResult.getAllErrors().get(0).getDefaultMessage());
        }

        return messageRepository.save(message);
    }
}

The test class files should be placed to the src/test/java folder and the name of the test class should have a Test prefix. For example, we can test the MessageRestController class with a MessageRestControllerTest class.

To make sure that the tests in the test class are independent, the setUp method should delete all messages at the beginning of each test:

package fi.haagahelia.quizzer.controller;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import com.fasterxml.jackson.databind.ObjectMapper;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import java.util.List;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import fi.haagahelia.quizzer.model.Message;
import fi.haagahelia.quizzer.repository.MessageRepository;

@SpringBootTest
@AutoConfigureMockMvc
public class MessageRestControllerTest {
    @Autowired
    MessageRepository messageRepository;

    @Autowired
    private MockMvc mockMvc;

    ObjectMapper mapper = new ObjectMapper();

    @BeforeEach
    void setUp() throws Exception {
        // Make sure that the database is empty before each test
        messageRepository.deleteAll();
    }

    // The test methods go here
}

The test methods test specific scenario. We come up with scenarios by analyzing the code (for example a certain method) that we are testing: how does the code behave based on different parameters or database state? For example if we call a method with certain parameters, we expect it to return a certain value. We need to cover all divergences in the code behavior with a test scenario.

For example, we could have the following test scenarios for the getAllMessages method introduced above:

  1. If we send a request to the /api/messages endpoint and there is no messages in the database, the JSON response body should be an empty list
  2. If we send a request to the /api/messages endpoint and there are messages in the database, the JSON response body should contain the messages as a list

To structure these test cases as test methods, we can follow the popular Arrange-Act-Assert pattern:

  1. Arrange inputs and targets. Arrange steps should set up the test case. Does the test require any objects or special settings? Does it need to prep a database? Does it need to log into a web app? Handle all of these operations at the start of the test
  2. Act on the target behavior. Act steps should cover the main thing to be tested. This could be calling a function or method, calling a REST API, or interacting with a web page. Keep actions focused on the target behavior
  3. Assert expected outcomes. Act steps should elicit some sort of response. Assert steps verify the goodness or badness of that response. Sometimes, assertions are as simple as checking numeric or string values. Other times, they may require checking multiple facets of a system. Assertions will ultimately determine if the test passes or fails

Here’s the two test methods for our test scenarios:

@Test
public void getAllMessagesReturnsEmptyListWhenNoMessagesExist() throws Exception {
    // Act
    this.mockMvc.perform(get("/api/messages"))
    // Assert
            .andExpect(status().isOk())
            .andExpect(jsonPath("$", hasSize(0)));
}

@Test
public void getAllMessagesReturnsListOfMessagesWhenMessagesExist() throws Exception {
    // Arrange
    Message firstMessage = new Message("First message");
    Message secondMessage = new Message("Second message");
    messageRepository.saveAll(List.of(firstMessage, secondMessage));

    // Act
    this.mockMvc.perform(get("/api/messages"))
    // Assert
            .andExpect(status().isOk())
            .andExpect(jsonPath("$", hasSize(2)))
            .andExpect(jsonPath("$[0].content").value("First message"))
            .andExpect(jsonPath("$[0].id").value(firstMessage.getId()))
            .andExpect(jsonPath("$[1].content").value("Second message"))
            .andExpect(jsonPath("$[1].id").value(secondMessage.getId()));
}

The tests use the perform method of the MockMVC class to send a GET request to the /api/messages endpoint. Then, we expect that the HTTP status of the response is 200 OK and the JSON of the response body contains the messages we saved in the arrange step.

For another GET request example, here’s the test methods of the getMessageById method introduced above:

@Test
public void getMessageByIdReturnsMessageWhenMessageExists() throws Exception {
    // Arrange
    Message message = new Message("Message");
    messageRepository.save(message);

    // Act
    this.mockMvc.perform(get("/api/messages/" + message.getId()))
    // Assert
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.content").value("Message"))
            .andExpect(jsonPath("$.id").value(message.getId()));
}

@Test
public void getMessageByIdReturnsNotFoundWhenMessageDoesNotExist() throws Exception {
    // Act
    this.mockMvc.perform(get("/api/messages/1"))
    // Assert
            .andExpect(status().isNotFound());
}

For a POST request example, here’s the test methods of the createMessage method introduced above:

@Test
public void createMessageSavesValidMessage() throws Exception {
    // Arrange
    Message message = new CreateMessageDto("Hello world!");
    String requestBody = mapper.writeValueAsString(message);

    // Act
    this.mockMvc.perform(post("/api/messages").contentType(MediaType.APPLICATION_JSON).content(requestBody))
    // Assert
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.content").value("Hello world!"));

    List<Message> messages = messageRepository.findAll();
    assertEquals(1, messages.size());
    assertEquals("Hello world!", messages.get(0).getContent());
}

@Test
public void createMessageDoesNotSaveInvalidMessage() throws Exception {
    // Arrange
    Message message = new CreateMessageDto("");
    String requestBody = mapper.writeValueAsString(message);

    // Act
    this.mockMvc.perform(post("/api/messages").contentType(MediaType.APPLICATION_JSON).content(requestBody))
    // Assert
            .andExpect(status().isBadRequest());

    List<Message> messages = messageRepository.findAll();
    assertEquals(0, messages.size());
}

The basic test functionalitis are provided by the spring-boot-starter-test library. Let’s add it to the <dependencies> list in the pom.xml file

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

The jsonPath method used to access the JSON payload in the test expectations is provided by the json-path libary. Before we start testing our REST API endpoints, let’s add the json-path dependency to the <dependencies> list in the pom.xml file:

<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
    <version>2.8.0</version>
    <scope>test</scope>
</dependency>

While testing your application’s REST API endpoints, refer to the examples above and guides, such as Integration Testing in Spring.

You can run the tests for the project either in Eclipse or by running the ./mvnw test command on the command-line.

Exercise 14

To classify test-related issues, create a new “test” label. Add the “test” label for issues that are related to testing (automated or manual) some part of the application.

If the implementation of the endpoint doesn’t work as described in the test scenarios, fix the implementation.

Exercise 15

Create a new package fi.haagahelia.quizzer.controller to the src/test/java folder for the project’s controller class tests. Implement a test class within the package with the following test methods for the endpoint for getting all (published) quizzes:

  • getAllQuizzesReturnsEmptyListWhenNoQuizzesExist: send a request without saving a quiz to the database. Then, the response should have an empty list
  • getAllQuizzesReturnsListOfPublishedQuizzesWhenQuizzesExist: save a few quizzes (both published and non-published) to the database and send a request. Then, the response should have a list of the saved published quizzes

Create an issue for this task.

Exercise 16

Implement appropriate test methods for the endpoint for getting all categories.

Create an issue for this task.

Exercise 17

Implement the following test methods for the endpoint for getting a quiz by id:

  • getQuizByIdReturnsPublishedQuizWhenQuizExists: save a quiz to the database and send a request. Then, the response should have the saved quiz
  • getQuizByIdReturnsErrorWhenQuizDoesNotExist: send a request without saving a quiz to the database. Then, the response should have an appropriate HTTP status

Create an issue for this task.

Exercise 18

Implement the following test methods for the endpoint for getting the questions of a quiz:

  • getQuestionsByQuizIdReturnsEmptyListWhenQuizDoesNotHaveQuestions: save a quiz without questions to the database and send a request. Then, the response should have an empty list
  • getQuestionsByQuizIdReturnsListOfQuestionsWhenQuizHasQuestions: save a quiz with a few questions to the database and send a request. Then, the response should have a list of the quiz’s questions
  • getQuestionsByQuizIdReturnsAnswerOptionsOfQuestions: save a quiz with a few questions and answer options to the database and send a request. Then, the response should include the list of answer options of the question
  • getQuestionsByQuizIdReturnsErrorWhenQuizDoesNotExist: send a request without saving a quiz to the database. Then, the response should have an appropriate HTTP status
  • An appropriate test methods for testing that the difficulty level filtering works. Implement separate test methods for success and error cases

Create an issue for this task.

Exercise 19

Implement a test class with the following test methods for the endpoint for creating an answer:

  • createAnswerSavesValidAnswer: save a published quiz with a question to the database and send a request with a valid request body (attributes should pass the validation). Then, the response should have the saved answer and the database should have one answer with the attributes matching the request body
  • createAnswerDoesNotSaveAnswerForNonExistingQuestion: send a request with a non-existing question id in the request body. Then, the response should have an appropriate HTTP status and the database should not have any answers
  • createAnswerDoesNotSaveAnswerForNonPublishedQuiz: save a non-published quiz with a question to the database and send a request with a valid request body. Then, the response should have an appropriate HTTP status and the database should not have any answers
  • createAnswerDoesNotSaveInvalidAnswer: send a request without an answer option id in the request body (for example set as null). Then, the response should have an appropriate HTTP status and the database should not have any answers

Create an issue for this task.

Exercise 20

Implement appropriate test methods for the endpoint for getting the answers of a quiz.

Create an issue for this task.

Exercise 21

Add instructions on how to run the tests on the command-line to the “Developer guide” section in the README.md file.

⭐ Bonus: Test coverage

We have analyzed the code that we are testing and we are quite sure that our test scenarios cover everything. The good news is, that we don’t need to trust only on our gut. There are so called test coverage tools that analyze which lines of code our test scenarios cover and which they don’t.

JaCoCo is one of the most widely used code coverage tools for Java. Let’s add the jacoco-maven-plugin to the <plugins> list in the pom.xml file:

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>

If we run the ./mvnw test command in Git Bash, our tests are executed and JaCoCo will generate a code coverage report. Once the command has finished successfully, the code coverage report can be found as an index.html file in the target/site/jacoco folder. Open the index.html file in a web browser.

The report displayes the coverage of each package. The “Cov” column determines the percentage of lines covered by the tests. Bigger the number, better the coverage. If we click a package name, we see the classes in the package. By clicking a class name, we see the methods of the class. By clicking a method we see the method’s implementation as code.

Green highlight indicates that the line is fully covered. Yellow highlight indicates that the line is partially covered. For example a certain condition of an if statement is not covered by a test. Red highlight indicates that line is not covered.

Software development teams commonly decide the minimum test coverage percentage for the code. The test coverage is automatically checked each time new code is pushed to the repository. If the new code doesn’t fullfill the minimum requirement, the code won’t be integrated to the main branch of the repository. This is one of the practices of continuous integration.

When we change the code (in the tests or in the application), we need to re-run ./mvnw test to generate the coverage report for the latest tests.

⭐ Bonus exercise

  1. Use the jacoco-maven-plugin in the project as instructed above
  2. Generate a coverage report and check the coverage of the tested methods. Are all the lines of the methods fully covered by the tests? If not, implement appropriate test cases to cover the not convered or partly covered lines of the code
  3. Implement more tests to increase the test coverage of the project

Authentication

Most of the application have features that need to verify the user’s identity before they are allowed to perform certain actions. This process is referred to as authentication. User’s identity can be verified in different ways, but a quite common process is to associate a password with a certain username or some other unique identifier such as email. The user who knows the password of a username will be identified as that user.

The user’s password is not stored to the database as a plain text, instead a hash presentation of the password is stored. Hash is like a secret we put behind a door, lock it with a key and throw the key away. There’s no way of getting the original text from a hash. But we can comprare two hashes and see if their value is the same. If the data in the database gets into wrong hands, the password hashes can’t be used to authenticate.

In web applications the common authentication flow goes like this:

  1. The user sends a request with a username and password to the server
  2. The server fetches the password hash with the given username from the database, hashes the provided password and compares it with the password hash in the database
  3. If the hashes match, the server provides the user with a token that they can use to authenticate the future requests. The server commonly puts the token to a cookie and it is sent to server in each request by the web browser

Spring Security is a popular authentication and access-control framework for Spring applications. Let’s start using Spring Security in our application by adding the dependency to the <dependencies> list in the pom.xml file:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

To use authentication related information in Thymeleaf templates, we can use the Thymeleaf Spring Security dialect. Let’s also add that dependency to the <dependencies> list in the pom.xml file:

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>

Next, we need to configure the Spring Security a bit. Let’s add the following SecurityConfig configuration class for our project:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests((requests) -> requests
            .requestMatchers(
                // The REST API endpoints
                antMatcher("/api/**"),
                // The error page
                antMatcher("/error"),
                // Swagger documentation paths
                antMatcher("/v3/api-docs/**"),
                antMatcher("/configuration/ui"),
                antMatcher("/swagger-resources/**"),
                antMatcher("/configuration/security"),
                antMatcher("/swagger-ui/**"))
                // Rest of the permitted paths
                // ...
            .permitAll()
            .anyRequest()
            .authenticated());

        http.formLogin((form) -> form.permitAll());
        http.logout((logout) -> logout.permitAll());
        http.cors(Customizer.withDefaults());
        http.csrf((csrf) -> csrf.ignoringRequestMatchers(antMatcher("/api/**")));

        return http.build();
    }
}

The passwordEncoder method returns the password encoder object used to hash passwords. We’ll use bcrypt which is the de facto hash algorithm for passwords.

The securityFilterChain returns the configuration object for Spring Security. The first piece of configuration determines the access-control for our application. The permitAll() method call will permit anyone to access these paths. This is follow by anyRequest().authenticated() method call, which means that request to any other path will require authentication.

On top of the configuration class, we need to have class that implements the UserDetailsService interface. This class will determine how to fetch the user’s information based on the username:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // ...
    }
}

The loadUserByUsername method will need to return a UserDetails object based on the username provided by the paramater or throw an UsernameNotFoundException exception if no user is found.

Exercise 22

Read the GitHub’s documentation on Licensing a repository. Then, choose a license for your repository and place the license text in a file named LICENSE at the root folder of your repository (the same folder that has the pom.xml file). If you don’t have a strong opinion on the license, you can consider the MIT license.

Add a “License” subheading to the README.md file and under that the chosen license name and the link to the LICENSE file in the GitHub repository. As a reference, you can take a look how the license is specified in the React project’s README.md file.

Exercise 23

Deploy the final versions of the backend and frontend applications to the production environment. Make sure that the applications work properly in the production environment.

Exercise 24

Make sure that all project-related documentation, such as project description, data model documentation, architecture documentation and Swagger documentation is up-to-date. Also, add a few screenshots of the most important features of the application to the README.md file’s project description to demonstrate what the application looks like. Here is a guide on how to use images in Markdown.

Exercise 25

Once you have implemented the user stories of the Sprint and the main branch has a working version of the application, create a GitHub release for the project. Create a new tag called “sprint3”. The release title should be “Sprint 3”. Give a brief description for the release that describes the features implemented during the Sprint.

Peer review

Writing a peer review for each team member and receiving a passing grade from the peer reviews is required to pass the course.

The peer review is used to assess each team member. The 10 personal points are based on the peer reviews and the teacher’s observations. Every team member must write a peer review.

The peer review is conducted with a form. You will receive the link to the form via email from the teacher at the beginning of the Sprint. In the form you will need to assess every team member’s (including yourself) efforts in the team work in the following aspects:

  • Activity in team work: Attendance and active presence during team meetings and communication with team members outside the meetings
  • Technical contributions: amount of working code written or active participation in the writing process of the code (for example pair-programming)
  • Project management and documentation contributions: Backlog management, efforts to improve the process (for example in Retrospectives), writing project related documentation

You will need to grade each these aspects in scale of 0-5 and provide a short reasoning for the grade. The peer reviews are anonymous, the team members won’t see each other’s peer reviews.

Exercise 26

Write the peer review for your team members. You will receive the peer review form via email. If you haven’t received the peer review form link, contact the teacher.

Final report

Write the final report for the course, which covers the following questions:

  • Scrum defines four events which take place during the Sprint. What are these events and what is the purpose of each event? How well did you succeed as a team to fulfil the purpose of each event?
  • Product backlog and Sprint backlog are perhaps the most important Scrum artifacts. What is their purpose? How well did your backlogs succeed to fulfil their purpose?
  • In which areas did you succeed as a team? In which areas there was room for improvement?
  • In which areas did you succeed personally? In which areas there was room for improvement?
  • Based on your experiences during the course, what would be three important advice you would give to a new software development team? Justify why these advice are important
  • What did you learn during the course? What would you have wanted to learn more about?

Submit the final report as a single PDF file to this Moodle submission.

Exercise 27

Write the final report as instructed above.

Make sure that everything mentioned in the exercises is pushed to the project’s GitHub repository before the Sprint 3 deadline on 20.5. at 11:00.