Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#12995] Create documentation for unit tests #12996

Merged
merged 5 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/_markbind/layouts/default.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
* [Captcha]({{ baseUrl }}/captcha.html)
* [Documentation]({{ baseUrl }}/documentation.html)
* [Emails]({{ baseUrl }}/emails.html)
* [Unit Testing]({{ baseUrl }}/unit-testing.html)
* [End-to-End Testing]({{ baseUrl }}/e2e-testing.html)
* [Performance Testing]({{ baseUrl }}/performance-testing.html)
* [Accessibility Testing]({{ baseUrl }}/axe-testing.html)
Expand Down
6 changes: 5 additions & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,9 @@ There are two big categories of testing in TEAMMATES:
- **Component tests**: white-box unit and integration tests, i.e. they test the application components with full knowledge of the components' internal workings. This is configured in `src/test/resources/testng-component.xml` (back-end) and `src/web/jest.config.js` (front-end).
- **E2E (end-to-end) tests**: black-box tests, i.e. they test the application as a whole without knowing any internal working. This is configured in `src/e2e/resources/testng-e2e.xml`. To learn more about E2E tests, refer to this [document](e2e-testing.md).

#### Running the tests
<div id="running-tests">

#### Running tests

##### Frontend tests

Expand Down Expand Up @@ -335,6 +337,8 @@ You can generate the coverage data with `jacocoReport` task after running tests,

The report can be found in the `build/reports/jacoco/jacocoReport/` directory.

</div>

## Deploying to a staging server

> `Staging server` is the server instance you set up on Google App Engine for hosting the app for testing purposes.
Expand Down
206 changes: 206 additions & 0 deletions docs/unit-testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
<frontmatter>
title: "Unit Testing"
</frontmatter>

# Unit Testing

## What is Unit Testing?

Unit testing is a testing methodology where the objective is to test components in isolation.

- It aims to ensure all components of the application work as expected, assuming its dependencies are working.
- This is done in TEAMMATES by using mocks to simulate a component's dependencies.

Frontend Unit tests in TEAMMATES are located in `.spec.ts` files, while Backend Unit tests in TEAMMATES can be found in the package `teammates.test`.


## Writing Unit Tests

### General guidelines

#### Include only relevant details in tests
When writing unit tests, reduce the amount of noise in the code to make it easier for future developers to follow.

The code below has alot of noise in creation of the `studentModel`:
cedricongjh marked this conversation as resolved.
Show resolved Hide resolved

```javascript
it('displayInviteButton: should display "Send Invite" button when a student has not joined the course', () => {
component.studentModels = [
{
student: {
name: 'tester',
teamName: 'Team 1',
email: '[email protected]',
joinState: JoinState.NOT_JOINED,
sectionName: 'Tutorial Group 1',
courseId: 'text-exa.demo',
},
isAllowedToViewStudentInSection: true,
isAllowedToModifyStudent: true,
},
];

expect(sendInviteButton).toBeTruthy();
});
```

However, what is important is only the student joinState. We should thus reduce the noise by including only the relevant details:

```javascript
it('displayInviteButton: should display "Send Invite" button when a student has not joined the course', () => {
component.studentModels = [
studentModelBuilder
.joinState(JoinState.NOT_JOINED)
.build()
];

expect(sendInviteButton).toBeTruthy();
});
```

By including only the relevant details in tests, it makes it easier for future developers to read and understand the purpose of the test.
cedricongjh marked this conversation as resolved.
Show resolved Hide resolved

#### Favor readability over uniqueness
Since tests don't have tests, it should be easy for developers to manually inspect them for correctness, even at the expense of greater code duplication.

Take the following test for example:

```java
@BeforeMethod
public void setUp() {
users = new User[]{new User("alice"), new User("bob")};
}

@Test
public void test_register_canRegisterMultipleUsers() {
registerAllUsers();
for (User user : users) {
assertTrue(forum.hasRegisteredUser(user));
}
}

private void registerAllUsers() {
for (User user : users) {
forum.register(user);
}
}
```

While the code reduces duplication, it is not as straightforward for a developer to follow.

A more readable way to write this test would be:
```java
@Test
public void test_register_canRegisterMultipleUsers() {
User user1 = new User("alice");
User user2 = new User("bob");

forum.register(user1);
forum.register(user2);

assertTrue(forum.hasRegisteredUser(user1));
assertTrue(forum.hasRegisteredUser(user2));
}
```

By choosing readability over uniqueness in writing unit tests, there is code duplication, but the test flow is easier for a reader to follow.


#### Inline mocks in test code

Inlining mock return values in the unit test itself improves readability:

```javascript
it('getStudentCourseJoinStatus: should return true if student has joined the course' , () => {
jest.spyOn(courseService, 'getJoinCourseStatus')
.mockReturnValue(of({ hasJoined: true }));

expect(student.getJoinCourseStatus).toBeTruthy();
});
```

By injecting the values in the test right before they are used, developers are able to more easily trace the code and understand the test.

### Frontend

#### Naming
Unit tests for a function should follow the format:

`"<function-name>: should ... when/if ..."`

Example:

```javascript
it('hasSection: should return false when there are no sections in the course')
```

#### Creating test data
To aid with [including only relevant details in tests](#include-only-relevant-details-in-tests), use the builder in `src/web/test-helpers/generic-builder.ts`

Usage:
```javascript
const instructorModelBuilder = createBuilder<InstructorListInfoTableRowModel>({
email: '[email protected]',
name: 'Instructor',
hasSubmittedSession: false,
isSelected: false,
});

it('isAllInstructorsSelected: should return false if at least one instructor !isSelected', () => {
component.instructorListInfoTableRowModels = [
instructorModelBuilder.isSelected(true).build(),
instructorModelBuilder.isSelected(false).build(),
instructorModelBuilder.isSelected(true).build(),
];

expect(component.isAllInstructorsSelected).toBeFalsy();
});

```

#### Testing event emission
In Angular, child components emit events. To test for event emissions, we've provided a utility function in `src/test-helpers/test-event-emitter`

Usage:
```javascript
@Output()
deleteCommentEvent: EventEmitter<number> = new EventEmitter();

triggerDeleteCommentEvent(index: number): void {
this.deleteCommentEvent.emit(index);
}

it('triggerDeleteCommentEvent: should emit the correct index to deleteCommentEvent', () => {
let emittedIndex: number | undefined;
testEventEmission(component.deleteCommentEvent, (index) => { emittedIndex = index; });

component.triggerDeleteCommentEvent(5);
expect(emittedIndex).toBe(5);
});
```

### Backend

#### Naming
Unit test names should follow the format: `test<functionName>_<scenario>_<outcome>`

Examples:
```java
public void testGetComment_commentDoesNotExist_returnsNull()
public void testCreateComment_commentDoesNotExist_success()
public void testCreateComment_commentAlreadyExists_throwsEntityAlreadyExistsException()
```

#### Creating test data
To aid with [including only relevant details in tests](#include-only-relevant-details-in-tests), use the `getTypicalX` functions in `BaseTestCase`, where X represents an entity.

Example:
```java
Account account = getTypicalAccount();
account.setEmail("[email protected]");

Student student = getTypicalStudent();
student.setName("New Student Name");
```

<include src="development.md#running-tests" />
Loading