From db0fd945b9fd03584fcac0cef53585373a02eca7 Mon Sep 17 00:00:00 2001
From: Cedric Ong <67156011+cedricongjh@users.noreply.github.com>
Date: Tue, 9 Apr 2024 04:28:02 +0800
Subject: [PATCH] [#12995] Create documentation for unit tests (#12996)
* Create documentation for unit tests
* Update docs/unit-testing.md
Co-authored-by: Zhang Ziqing <69516975+ziqing26@users.noreply.github.com>
* Update docs/unit-testing.md
Co-authored-by: Zhang Ziqing <69516975+ziqing26@users.noreply.github.com>
---------
Co-authored-by: Zhang Ziqing <69516975+ziqing26@users.noreply.github.com>
---
docs/_markbind/layouts/default.md | 1 +
docs/development.md | 6 +-
docs/unit-testing.md | 206 ++++++++++++++++++++++++++++++
3 files changed, 212 insertions(+), 1 deletion(-)
create mode 100644 docs/unit-testing.md
diff --git a/docs/_markbind/layouts/default.md b/docs/_markbind/layouts/default.md
index f92791366a0..e57ee418a49 100644
--- a/docs/_markbind/layouts/default.md
+++ b/docs/_markbind/layouts/default.md
@@ -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)
diff --git a/docs/development.md b/docs/development.md
index 703a1ce57b0..aec8c3696ca 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -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
+
+
+#### Running tests
##### Frontend tests
@@ -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.
+
+
## 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.
diff --git a/docs/unit-testing.md b/docs/unit-testing.md
new file mode 100644
index 00000000000..fe4d2786c8e
--- /dev/null
+++ b/docs/unit-testing.md
@@ -0,0 +1,206 @@
+
+ title: "Unit Testing"
+
+
+# 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 a lot of noise in creation of the `studentModel`:
+
+```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: 'tester@tester.com',
+ 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();
+});
+```
+
+Including only the relevant details in tests makes it easier for future developers to read and understand the purpose of the test.
+
+#### 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:
+
+`": 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({
+ email: 'instructor@gmail.com',
+ 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 = 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__`
+
+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("newemail@teammates.com");
+
+Student student = getTypicalStudent();
+student.setName("New Student Name");
+```
+
+