Skip to content

Commit 3c92a43

Browse files
committed
feat: lab3
1 parent dc76ca7 commit 3c92a43

5 files changed

Lines changed: 111 additions & 155 deletions

File tree

lab2/main_test.js

Lines changed: 5 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -1,156 +1,6 @@
1-
import fs from 'fs';
2-
import path from 'path';
3-
import os from 'os';
4-
import test from 'node:test';
5-
import assert from 'assert';
6-
import { Application, MailSystem } from './main.js';
1+
const test = require('node:test');
2+
const assert = require('assert');
3+
const { Application, MailSystem } = require('./main');
74

8-
// 輔助函式:在暫存目錄中建立 name_list.txt,並切換工作目錄
9-
async function withTempNameList(fn) {
10-
const originalCwd = process.cwd();
11-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lab2-'));
12-
fs.writeFileSync(path.join(tmpDir, 'name_list.txt'), 'Alice\nBob\nCharlie');
13-
process.chdir(tmpDir);
14-
try {
15-
await fn();
16-
} finally {
17-
process.chdir(originalCwd);
18-
fs.rmSync(tmpDir, { recursive: true, force: true });
19-
}
20-
}
21-
22-
// 建立 Stub 輔助函式
23-
const createStub = (obj, method, implementation) => {
24-
const original = obj[method];
25-
obj[method] = implementation;
26-
return { restore: () => { obj[method] = original; } };
27-
};
28-
29-
// 測試 MailSystem.write()(不需要檔案)
30-
test('MailSystem.write should generate correct mail content', () => {
31-
const mailSystem = new MailSystem();
32-
const result = mailSystem.write('Alice');
33-
assert.strictEqual(result, 'Congrats, Alice!');
34-
});
35-
36-
// 測試 MailSystem.send():透過 stub 控制隨機行為
37-
test('MailSystem.send should return success or failure', () => {
38-
const mailSystem = new MailSystem();
39-
const randomStub = createStub(Math, 'random', () => 0.9);
40-
assert.strictEqual(mailSystem.send('Alice', 'Congrats, Alice!'), true);
41-
randomStub.restore();
42-
const randomStub2 = createStub(Math, 'random', () => 0.4);
43-
assert.strictEqual(mailSystem.send('Alice', 'Congrats, Alice!'), false);
44-
randomStub2.restore();
45-
});
46-
47-
// 以下測試皆需要 name_list.txt,所以採用 withTempNameList 包裹
48-
49-
test('Application.getNames should read and parse names from file', async () => {
50-
await withTempNameList(async () => {
51-
const app = new Application();
52-
// 等待 constructor 的非同步初始化完成
53-
await new Promise(resolve => setTimeout(resolve, 10));
54-
const [people, selected] = await app.getNames();
55-
assert.deepStrictEqual(people, ['Alice', 'Bob', 'Charlie']);
56-
assert.deepStrictEqual(selected, []);
57-
});
58-
});
59-
60-
test('Application constructor should initialize people and selected', async () => {
61-
await withTempNameList(async () => {
62-
const app = new Application();
63-
await new Promise(resolve => setTimeout(resolve, 10));
64-
assert.deepStrictEqual(app.people, ['Alice', 'Bob', 'Charlie']);
65-
assert.deepStrictEqual(app.selected, []);
66-
});
67-
});
68-
69-
test('Application.getRandomPerson should return a person from people list', async () => {
70-
await withTempNameList(async () => {
71-
const app = new Application();
72-
await new Promise(resolve => setTimeout(resolve, 10));
73-
// stub Math.random 固定回傳 0.5 (floor(0.5 * 3) = 1) → 'Bob'
74-
const randomStub = createStub(Math, 'random', () => 0.5);
75-
assert.strictEqual(app.getRandomPerson(), 'Bob');
76-
randomStub.restore();
77-
});
78-
});
79-
80-
test('Application.selectNextPerson should select a unique person in order', async () => {
81-
await withTempNameList(async () => {
82-
const app = new Application();
83-
await new Promise(resolve => setTimeout(resolve, 10));
84-
// stub getRandomPerson 依序回傳:'Alice', 'Bob', 'Charlie'
85-
let sequence = ['Alice', 'Bob', 'Charlie'];
86-
let index = 0;
87-
const stub = createStub(app, 'getRandomPerson', () => sequence[index++]);
88-
89-
const selected1 = app.selectNextPerson();
90-
assert.strictEqual(selected1, 'Alice');
91-
assert.strictEqual(app.selected.length, 1);
92-
93-
const selected2 = app.selectNextPerson();
94-
assert.strictEqual(selected2, 'Bob');
95-
assert.strictEqual(app.selected.length, 2);
96-
97-
const selected3 = app.selectNextPerson();
98-
assert.strictEqual(selected3, 'Charlie');
99-
assert.strictEqual(app.selected.length, 3);
100-
101-
const selected4 = app.selectNextPerson();
102-
assert.strictEqual(selected4, null);
103-
104-
stub.restore();
105-
});
106-
});
107-
108-
test('Application.selectNextPerson should retry if duplicate selected', async () => {
109-
await withTempNameList(async () => {
110-
const app = new Application();
111-
await new Promise(resolve => setTimeout(resolve, 10));
112-
// stub 模擬第一次回傳 'Alice'(已被選取),接著回傳 'Bob'
113-
let callCount = 0;
114-
const stub = createStub(app, 'getRandomPerson', () => {
115-
if (callCount === 0) {
116-
callCount++;
117-
return 'Alice';
118-
}
119-
return 'Bob';
120-
});
121-
// 預先將 'Alice' 加入已選名單
122-
app.selected = ['Alice'];
123-
const result = app.selectNextPerson();
124-
assert.strictEqual(result, 'Bob');
125-
stub.restore();
126-
});
127-
});
128-
129-
test('Application.notifySelected should send mail to selected people', async () => {
130-
await withTempNameList(async () => {
131-
const app = new Application();
132-
await new Promise(resolve => setTimeout(resolve, 10));
133-
app.selected = ['Alice', 'Bob'];
134-
let writeCalls = [];
135-
let sendCalls = [];
136-
const writeStub = createStub(app.mailSystem, 'write', (name) => {
137-
writeCalls.push(name);
138-
return `Congrats, ${name}!`;
139-
});
140-
const sendStub = createStub(app.mailSystem, 'send', (name, content) => {
141-
sendCalls.push({ name, content });
142-
return true;
143-
});
144-
145-
app.notifySelected();
146-
147-
assert.deepStrictEqual(writeCalls, ['Alice', 'Bob']);
148-
assert.deepStrictEqual(sendCalls, [
149-
{ name: 'Alice', content: 'Congrats, Alice!' },
150-
{ name: 'Bob', content: 'Congrats, Bob!' }
151-
]);
152-
153-
writeStub.restore();
154-
sendStub.restore();
155-
});
156-
});
5+
// TODO: write your tests here
6+
// Remember to use Stub, Mock, and Spy when necessary

lab3/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Lab3
2+
3+
## Introduction
4+
5+
In this lab, you will write unit tests for functions implemented in `main.js`. You can learn how to use classes and functions in it by uncommenting the code in it. (But remember don't commit them on GitHub)
6+
7+
## Preparation (Important!!!)
8+
9+
1. Sync fork on GitHub
10+
2. `git checkout -b lab3` (**NOT** your student ID !!!)
11+
12+
## Requirement
13+
14+
1. (40%) Write test cases in `main_test.js` and achieve 100% code coverage.
15+
2. (30%) For each function, parameterize their testcases to test the error-results.
16+
3. (30%) For each function, use at least 3 parameterized testcases to test the non-error-results.
17+
18+
You can run `validate.sh` in your local to test if you satisfy the requirements.
19+
20+
Please note that you must not alter files other than `main_test.js`. You will get 0 points if
21+
22+
1. you modify other files to achieve requirements.
23+
2. you can't pass all CI on your PR.
24+
25+
## Submission
26+
27+
You need to open a pull request to your branch (e.g. 312XXXXXX, your student number) and contain the code that satisfies the abovementioned requirements.
28+
29+
Moreover, please submit the URL of your PR to E3. Your submission will only be accepted when you present at both places.

lab3/main.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
class Calculator {
2+
exp(x) {
3+
if (!Number.isFinite(x)) {
4+
throw Error('unsupported operand type');
5+
}
6+
const result = Math.exp(x);
7+
if (result === Infinity) {
8+
throw Error('overflow');
9+
}
10+
return result;
11+
}
12+
13+
log(x) {
14+
if (!Number.isFinite(x)) {
15+
throw Error('unsupported operand type');
16+
}
17+
const result = Math.log(x);
18+
if (result === -Infinity) {
19+
throw Error('math domain error (1)');
20+
}
21+
if (Number.isNaN(result)) {
22+
throw Error('math domain error (2)');
23+
}
24+
return result;
25+
}
26+
}
27+
28+
// const calculator = new Calculator();
29+
// console.log(calculator.exp(87));
30+
// console.log(calculator.log(48763));
31+
32+
module.exports = {
33+
Calculator
34+
};

lab3/main_test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const {describe, it} = require('node:test');
2+
const assert = require('assert');
3+
const { Calculator } = require('./main');
4+
5+
// TODO: write your tests here

lab3/validate.sh

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/bin/bash
2+
3+
# Check for unwanted files
4+
for file in *; do
5+
if [[ $file != "main.js" && $file != "main_test.js" && $file != "README.md" && $file != "validate.sh" ]]; then
6+
echo "[!] Unwanted file detected: $file."
7+
exit 1
8+
fi
9+
done
10+
11+
node=$(which node)
12+
test_path="${BASH_SOURCE[0]}"
13+
solution_path="$(realpath .)"
14+
tmp_dir=$(mktemp -d -t lab3-XXXXXXXXXX)
15+
16+
cd $tmp_dir
17+
18+
rm -rf *
19+
cp $solution_path/*.js .
20+
result=$($"node" --test --experimental-test-coverage) ; ret=$?
21+
if [ $ret -ne 0 ] ; then
22+
echo "[!] testing fails"
23+
exit 1
24+
else
25+
coverage=$(echo "$result" | grep 'all files' | awk -F '|' '{print $2}' | sed 's/ //g')
26+
if (( $(echo "$coverage < 100" | bc -l) )); then
27+
echo "[!] Coverage is only $coverage%"
28+
exit 1
29+
else
30+
echo "[V] Coverage is 100%"
31+
fi
32+
fi
33+
34+
rm -rf $tmp_dir
35+
36+
exit 0
37+
38+
# vim: set fenc=utf8 ff=unix et sw=2 ts=2 sts=2:

0 commit comments

Comments
 (0)