Skip to content

Commit f44ed6e

Browse files
More tests
1 parent ffe99a3 commit f44ed6e

File tree

11 files changed

+1712
-66
lines changed

11 files changed

+1712
-66
lines changed

package-lock.json

Lines changed: 1515 additions & 57 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,18 @@
2020
"devDependencies": {
2121
"@eslint/eslintrc": "3.3.0",
2222
"@tailwindcss/postcss": "4.0.14",
23+
"@testing-library/dom": "^10.4.0",
24+
"@testing-library/jest-dom": "^6.6.3",
25+
"@testing-library/react": "^16.2.0",
26+
"@testing-library/user-event": "14.6.1",
2327
"@types/node": "20",
24-
"@types/react": "19.0.10",
25-
"@types/react-dom": "19.0.4",
28+
"@types/react": "^19.0.10",
29+
"@types/react-dom": "^19.0.4",
30+
"@vitejs/plugin-react": "^4.3.4",
2631
"eslint": "9.22.0",
2732
"eslint-config-next": "15.2.1",
33+
"jsdom": "26.0.0",
34+
"jsdom-testing-mocks": "1.13.1",
2835
"prettier": "3.5.3",
2936
"tailwindcss": "4.0.14",
3037
"typescript": "5.8.2",
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { describe, expect, it, vitest } from "vitest";
2+
import React from "react";
3+
import { getMockBohne } from "@/test/mockData";
4+
import { render } from "@/test/userEvent";
5+
import { Bohnen } from "@/components/bohnen/Bohnen";
6+
import * as BohnenRowModule from "@/components/bohnen/BohnenRow";
7+
8+
const useContextMock = vitest.hoisted(() => vitest.fn());
9+
// Hier mocken wir das react Modul, damit useContext unseren dispatchMock zurückgibt
10+
// der Rest wird mit importOriginal() aus dem Originalmodul geladen
11+
vitest.mock("react", async (importOriginal) => ({
12+
...(await importOriginal()),
13+
useContext: useContextMock,
14+
}));
15+
16+
vitest.mock("next-intl", () => ({
17+
useTranslations: () => (key: string) => key,
18+
}));
19+
20+
describe("Bohnen", () => {
21+
it("Should render a BohnenRow for each bohne", () => {
22+
// Arrange
23+
const bohne1 = getMockBohne({ id: "1" });
24+
const bohne2 = getMockBohne({ id: "2" });
25+
useContextMock.mockReturnValue({ bohnen: [bohne1, bohne2] });
26+
27+
const bohnenRowSpy = vitest
28+
.spyOn(BohnenRowModule, "default")
29+
.mockImplementation(() => <></>);
30+
31+
// Act
32+
render(<Bohnen />);
33+
34+
// Assert
35+
expect(bohnenRowSpy).toHaveBeenCalledTimes(2);
36+
expect(bohnenRowSpy).toHaveBeenCalledWith({ bohne: bohne1 }, undefined);
37+
expect(bohnenRowSpy).toHaveBeenCalledWith({ bohne: bohne2 }, undefined);
38+
});
39+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React from "react";
2+
import { describe, expect, it, vitest } from "vitest";
3+
import BohnenRow from "@/components/bohnen/BohnenRow";
4+
import { getMockBohne } from "@/test/mockData";
5+
import { screen } from "@testing-library/react";
6+
import { render } from "@/test/userEvent";
7+
8+
const dispatchMock = vitest.hoisted(() => vitest.fn());
9+
10+
// Hier mocken wir das react Modul, damit useContext unseren dispatchMock zurückgibt
11+
// der Rest wird mit importOriginal() aus dem Originalmodul geladen
12+
vitest.mock("react", async (importOriginal) => ({
13+
...(await importOriginal()),
14+
useContext: () => dispatchMock,
15+
}));
16+
17+
describe("BohnenRow", () => {
18+
it("should render an input field to change the Bohnenart", () => {
19+
const mockBohne = getMockBohne();
20+
render(
21+
<table>
22+
<tbody>
23+
<BohnenRow bohne={mockBohne} />
24+
</tbody>
25+
</table>,
26+
);
27+
const input = screen.getByLabelText("Bohnenart");
28+
expect(input).toBeVisible();
29+
});
30+
31+
it("should dispatch an UPDATE event if the user clears the input for Bohnenart", async () => {
32+
const mockBohne = getMockBohne();
33+
const { user } = render(
34+
<table>
35+
<tbody>
36+
<BohnenRow bohne={mockBohne} />
37+
</tbody>
38+
</table>,
39+
);
40+
const input = screen.getByLabelText("Bohnenart");
41+
42+
await user.clear(input);
43+
await user.type(input, "test");
44+
45+
expect(dispatchMock).toHaveBeenCalledWith({
46+
type: "UPDATE",
47+
payload: { ...mockBohne, art: "" },
48+
});
49+
});
50+
});

src/components/bohnen/BohnenRow.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ const BohnenRow = ({ bohne }: BohnenRowProps) => {
1616
<input
1717
type="text"
1818
data-testid="art"
19+
aria-label="Bohnenart"
1920
className="border-2 border-slate-400"
20-
onChange={(event) =>
21+
onChange={(event) => {
22+
console.log("event.target.value", event.target.value);
2123
dispatch({
2224
type: BohnenActionTypes.UPDATE,
2325
payload: { ...bohne, art: event.target.value },
24-
})
25-
}
26+
});
27+
}}
2628
value={bohne.art || ""}
2729
/>
2830
</td>

src/state/calculate.as-mock-pattern.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ import { calculate } from "@/state/calculate";
33
import { getMockBohne } from "@/test/mockData";
44
import { predictPrice } from "@papperlapappyt/papperlapapp-coffee-prediction";
55

6+
// Verschiedene Verhaltensweisen mocken
7+
// Variante: asMock Pattern
8+
// - Nutzt autoMocking mit vitest.mock
9+
// - import der nun gemockten Methode unter dem Wissen, dass wir nun eine als Spy gewrappte
10+
// Methode haben, deren Verhalten wir beliebig steuern können
11+
12+
// Vorteil:
13+
// - Einfach zu nutzen
14+
// Nachteil:
15+
// - das Automocking passiert nur initial einmal beim Laden der Datei
16+
// => ein vitest.restoreAllMocks löscht den Spy und wir können die Methode nicht mehr steuern
17+
618
vitest.mock("@papperlapappyt/papperlapapp-coffee-prediction", () => ({
719
predictPrice: vitest.fn(),
820
}));

src/state/calculate.import-module-pattern.test.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,23 @@ import { calculate } from "@/state/calculate";
33
import { getMockBohne } from "@/test/mockData";
44
import * as predictPriceModule from "@papperlapappyt/papperlapapp-coffee-prediction";
55

6-
// vitest.mock hier nur, weil predictPrice als read-only property aus dem
7-
// prediction module exportiert wird.
8-
// Ansonsten bräuchten wir bei diesem Pattern kein vitest.mock.
6+
// Verschiedene Verhaltensweisen mocken
7+
// Variante: importModule Pattern
8+
// - Nutzt vitest.spyOn um die Methode zu mocken
9+
// - Dafür werden alle exports der Datei mit * in ein Objekt importiert,
10+
// um dieses Objekt mit spyOn nutzen zu können
11+
12+
// Vorteil:
13+
// - Ich bin unabhängig von vitest.restoreAllMocks
14+
// Nachteil:
15+
// - Etwas mehr Boilerplate als Variante beim "as Mock" Pattern
16+
// - vitest.spyOn kann keine read-only properties mocken
17+
// => Das ist hier im konkreten Beispiel der Fall. Als workaround wird das
18+
// Pattern mit vitest.mock kombiniert.
19+
// vitest.mock sorgt hier dafür, dass die readOnly property predictPrice
20+
// durch einen Mock ersetzt wird der dann nicht mehr readOnly ist.
21+
// Ansonsten bräuchten wir bei diesem Pattern kein vitest.mock!
22+
923
vitest.mock("@papperlapappyt/papperlapapp-coffee-prediction");
1024

1125
describe("tests mit 'as Mock' pattern", () => {

src/test/mockData.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Bohne } from "@/state/state";
22

3-
export const getMockBohne = (): Bohne => ({
3+
export const getMockBohne = (overrides?: Partial<Bohne>): Bohne => ({
44
id: "1",
55
ekp: 10,
66
marge: 10,
@@ -9,4 +9,5 @@ export const getMockBohne = (): Bohne => ({
99
vkp: 20,
1010
art: "test",
1111
vkpRabatt: 0,
12+
...overrides,
1213
});

src/test/userEvent.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import userEvent from "@testing-library/user-event";
2+
import { render as testingLibraryRender } from "@testing-library/react";
3+
import React from "react";
4+
import { vitest } from "vitest";
5+
6+
// setup function
7+
export function render(
8+
jsx: React.JSX.Element,
9+
useFakeTimers?: "useFakeTimers",
10+
) {
11+
const options =
12+
useFakeTimers === "useFakeTimers"
13+
? {
14+
advanceTimers: vitest.advanceTimersByTime.bind(vitest),
15+
}
16+
: undefined;
17+
18+
if (useFakeTimers) {
19+
// @ts-expect-error Workaround für Inkompatibilität zw. vitest mock timer und user event click:
20+
// https://github.com/testing-library/user-event/issues/1115
21+
globalThis.jest = {
22+
advanceTimersByTime: vitest.advanceTimersByTime.bind(vitest),
23+
};
24+
}
25+
26+
return {
27+
user: userEvent.setup(options),
28+
// Import `render` from the framework library of your choice.
29+
// See https://testing-library.com/docs/dom-testing-library/install#wrappers
30+
...testingLibraryRender(jsx, {
31+
wrapper: (props) => props.children,
32+
}),
33+
};
34+
}

vitest-setup.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as JestDomMatchers from "@testing-library/jest-dom/matchers";
2+
import { expect, vitest } from "vitest";
3+
import "@testing-library/jest-dom/vitest";
4+
5+
expect.extend({ ...JestDomMatchers });
6+
7+
const { error } = console;
8+
9+
console.error = vitest.fn((message, ...restArgs) => {
10+
error.apply(console, [message, ...restArgs]); // keep default behaviour
11+
throw message instanceof Error ? message : new Error(message);
12+
});
13+
14+
const { warn } = console;
15+
16+
console.warn = (message, ...restArgs) => {
17+
warn.apply(console, [message, ...restArgs]); // keep default behaviour
18+
throw message instanceof Error ? message : new Error(message);
19+
};

0 commit comments

Comments
 (0)