5 Commits

Author SHA1 Message Date
Dustin Tauer
e4ab8518f4 Merge pull request #1 from stevekinney/dustin/course-updates
adding readme, fixes
2024-10-23 10:48:30 -05:00
dtauer
c0fad99503 updated readme 2024-10-23 10:46:54 -05:00
dtauer
c89bb10b06 added readme, removed test dir 2024-10-23 10:45:31 -05:00
Steve Kinney
d9327443f2 Remove unused file 2024-10-08 17:04:23 -06:00
Steve Kinney
a3bcc619ff Update character.test.ts 2024-10-02 12:54:30 -05:00
16 changed files with 77 additions and 333 deletions

16
README.md Normal file
View File

@@ -0,0 +1,16 @@
## Testing Fundamentals Course
This is a companion repository for the [Testing Fundamentals](https://frontendmasters.com/courses/testing/) course on Frontend Masters.
[![Frontend Masters](https://static.frontendmasters.com/assets/brand/logos/full.png)](https://frontendmasters.com/courses/testing/)
## Setup Instructions
> We recommend using Node.js version 20+ for this course
Clone this repository and install the dependencies:
```bash
git clone https://github.com/stevekinney/introduction-to-testing.git
cd introduction-to-testing
npm install
```

View File

@@ -1,82 +1,34 @@
import { render, screen, act } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { Counter } from './counter'; import { Counter } from './counter';
import '@testing-library/jest-dom/vitest'; import '@testing-library/jest-dom/vitest';
describe('Counter ', () => { describe.todo('Counter ', () => {
it('renders with an initial count of 0', () => { beforeEach(() => {
render(<Counter />); render(<Counter />);
const counter = screen.getByTestId('counter-count');
expect(counter).toHaveTextContent('0');
}); });
it('disables the "Decrement" and "Reset" buttons when the count is 0', () => { it('renders with an initial count of 0');
render(<Counter />);
const decrementButton = screen.getByRole('button', { name: /decrement/i });
const resetButton = screen.getByRole('button', { name: /reset/i });
expect(decrementButton).toBeDisabled(); it('disables the "Decrement" and "Reset" buttons when the count is 0');
expect(resetButton).toBeDisabled();
});
it('displays "days" when the count is 0', () => { it.todo('displays "days" when the count is 0', () => {});
render(<Counter />);
const unit = screen.getByTestId('counter-unit');
expect(unit).toHaveTextContent('days');
});
it('increments the count when the "Increment" button is clicked', async () => { it.todo(
render(<Counter />); 'increments the count when the "Increment" button is clicked',
const incrementButton = screen.getByRole('button', { name: /increment/i }); async () => {},
const counter = screen.getByTestId('counter-count'); );
await act(async () => { it.todo('displays "day" when the count is 1', async () => {});
await userEvent.click(incrementButton);
});
expect(counter).toHaveTextContent('1'); it.todo(
}); 'decrements the count when the "Decrement" button is clicked',
async () => {},
);
it('displays "day" when the count is 1', async () => { it.todo('does not allow decrementing below 0', async () => {});
render(<Counter />);
const incrementButton = screen.getByRole('button', { name: /increment/i });
const unit = screen.getByTestId('counter-unit');
await act(async () => {
await userEvent.click(incrementButton);
});
expect(unit).toHaveTextContent('day');
});
it('decrements the count when the "Decrement" button is clicked', async () => {
render(<Counter initialCount={1} />);
const decrementButton = screen.getByRole('button', { name: /decrement/i });
const count = screen.getByTestId('counter-count');
expect(decrementButton).not.toBeDisabled();
await act(async () => {
await userEvent.click(decrementButton);
});
expect(count).toHaveTextContent('0');
expect(decrementButton).toBeDisabled();
});
it('does not allow decrementing below 0', async () => {
render(<Counter />);
const decrementButton = screen.getByRole('button', { name: /decrement/i });
const count = screen.getByTestId('counter-count');
await act(async () => {
await userEvent.click(decrementButton);
});
expect(count).toHaveTextContent('0');
});
it.todo( it.todo(
'resets the count when the "Reset" button is clicked', 'resets the count when the "Reset" button is clicked',
@@ -88,14 +40,5 @@ describe('Counter ', () => {
() => {}, () => {},
); );
it('updates the document title based on the count', async () => { it.todo('updates the document title based on the count', async () => {});
const { getByRole } = render(<Counter />);
const incrementButton = getByRole('button', { name: /increment/i });
await act(async () => {
await userEvent.click(incrementButton);
});
expect(document.title).toEqual(expect.stringContaining('1 day'));
});
}); });

View File

@@ -2,8 +2,8 @@ import React from 'react';
import { useReducer, useEffect } from 'react'; import { useReducer, useEffect } from 'react';
import { reducer } from './reducer'; import { reducer } from './reducer';
export const Counter = ({ initialCount = 0 }) => { export const Counter = () => {
const [state, dispatch] = useReducer(reducer, { count: initialCount }); const [state, dispatch] = useReducer(reducer, { count: 0 });
const unit = state.count === 1 ? 'day' : 'days'; const unit = state.count === 1 ? 'day' : 'days';
useEffect(() => { useEffect(() => {

View File

@@ -1,28 +1,7 @@
export const add = (a, b) => { export const add = () => {};
if (typeof a === 'string') a = Number(a);
if (typeof b === 'string') b = Number(b);
if (isNaN(a)) throw new Error('The first argument is not a number'); export const subtract = () => {};
if (isNaN(b)) throw new Error('The second argument is not a number');
return a + b; export const multiply = () => {};
};
export const subtract = (a = 0, b = 0) => { export const divide = () => {};
if (Array.isArray(a)) {
a = a.reduce((a, b) => {
return a - b;
});
}
return a - b;
};
export const multiply = (a, b) => {
return a * b;
};
export const divide = (a, b) => {
if (b === 0) return null;
return a / b;
};

View File

@@ -1,72 +1,9 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { add, subtract, multiply, divide } from './arithmetic.js';
describe('add', () => { describe.todo('add', () => {});
it('should add two positive numbers', () => {
expect(add(2, 2)).toBe(4);
});
it('should add two negative numbers', () => { describe.todo('subtract', () => {});
expect(add(-2, -2)).toBe(-4);
});
it('should parse strings into numbers', () => { describe.todo('multiply', () => {});
expect(add('1', '1')).toBe(2);
});
it('should get real angry if you give it a first argument that cannot be parsed into a number', () => { describe.todo('divide', () => {});
expect(() => add('potato', 2)).toThrow('not a number');
});
it('should get real angry if you give it a second argument that cannot be parsed into a number', () => {
expect(() => add(2, 'potato')).toThrow('not a number');
});
it('should throw if the first argument is not a number', () => {
expect(() => add(NaN, 2)).toThrow('not a number');
});
it('should handle floating point math as best it can', () => {
expect(add(1.0000001, 2.0000004)).toBeCloseTo(3.0, 1);
});
});
describe('subtract', () => {
it('should subtract one number from the other', () => {
expect(subtract(4, 2)).toBe(2);
});
it('should accept and subtract all of the numbers', () => {
expect(subtract([10, 5], 2)).toBe(3);
});
it('should default undefined values to 0', () => {
expect(subtract(3)).toBe(3);
expect(subtract(undefined, 3)).toBe(-3);
});
it('should default to zero if either argument is null', () => {
expect(subtract(3, null)).toBe(3);
expect(subtract(null, 3)).toBe(-3);
});
});
describe('multiply', () => {
it('should multiply two numbers', () => {
expect(multiply(3, 2)).toBe(6);
});
});
describe('divide', () => {
it('should divide two numbers', () => {
expect(divide(10, 2)).toBe(5);
});
it('should return null if dividing by zero', () => {
expect(divide(10, 0)).toBeNull();
});
it('should return zero if dividing by Infinity', () => {
expect(divide(10, Infinity)).toBe(0);
});
});

View File

@@ -2,21 +2,21 @@ import { Person } from './person.js';
import { rollDice } from './roll-dice.js'; import { rollDice } from './roll-dice.js';
export class Character extends Person { export class Character extends Person {
constructor(firstName, lastName, role, level = 1, roll = rollDice) { constructor(firstName, lastName, role) {
super(firstName, lastName); super(firstName, lastName);
this.role = role; this.role = role;
this.level = level; this.level = 1;
this.createdAt = new Date(); this.createdAt = new Date();
this.lastModified = this.createdAt; this.lastModified = this.createdAt;
this.strength = roll(4, 6); this.strength = rollDice(4, 6);
this.dexterity = roll(4, 6); this.dexterity = rollDice(4, 6);
this.intelligence = roll(4, 6); this.intelligence = rollDice(4, 6);
this.wisdom = roll(4, 6); this.wisdom = rollDice(4, 6);
this.charisma = roll(4, 6); this.charisma = rollDice(4, 6);
this.constitution = roll(4, 6); this.constitution = rollDice(4, 6);
} }
levelUp() { levelUp() {

View File

@@ -1,60 +1,14 @@
import { describe, it, expect, vi } from 'vitest'; import { describe, it, expect } from 'vitest';
import { Character } from './character.js'; import { Character } from './character.js';
import { Person } from './person.js'; import { Person } from './person.js';
const firstName = 'Ada';
const lastName = 'Lovelace';
const role = 'Computer Scienst';
describe('Character', () => { describe('Character', () => {
let character; it.todo(
'should create a character with a first name, last name, and role',
() => {},
);
beforeEach(() => { it.todo('should allow you to increase the level', () => {});
character = new Character(firstName, lastName, role, 1);
});
it.skip('should create a character with a first name, last name, and role', () => { it.todo('should update the last modified date when leveling up', () => {});
expect(character).toEqual({
firstName,
lastName,
role,
strength: 12,
wisdom: 12,
dexterity: 12,
intelligence: 12,
constitution: 12,
charisma: 12,
level: 1,
lastModified: expect.any(Date),
createdAt: expect.any(Date),
id: expect.stringContaining('person-'),
});
});
it('should allow you to increase the level', () => {
const initialLevel = character.level;
character.levelUp();
expect(character.level).toBeGreaterThan(initialLevel);
});
it('should update the last modified date when leveling up', () => {
const initialLastModified = character.lastModified;
character.levelUp();
expect(character.lastModified).not.toBe(initialLastModified);
});
it.only('should roll four six-sided die', () => {
const rollDiceMock = vi.fn(() => 15);
const character = new Character(firstName, lastName, role, 1, rollDiceMock);
expect(character.strength).toBe(15);
expect(rollDiceMock).toHaveBeenCalledWith(4, 6);
expect(rollDiceMock).toHaveBeenCalledTimes(6);
console.log(rollDiceMock.mock.calls);
});
}); });

View File

@@ -2,12 +2,11 @@ import { describe, it, expect } from 'vitest';
import { Person } from './person.js'; import { Person } from './person.js';
// Remove the `todo` from the `describe` to run the tests. // Remove the `todo` from the `describe` to run the tests.
describe('Person', () => { describe.todo('Person', () => {
// This test will fail. Why? // This test will fail. Why?
it('should create a person with a first name and last name', () => { it('should create a person with a first name and last name', () => {
const person = new Person('Grace', 'Hopper'); const person = new Person('Grace', 'Hopper');
expect(person).toEqual({ expect(person).toEqual({
id: expect.stringContaining('person-'),
firstName: 'Grace', firstName: 'Grace',
lastName: 'Hopper', lastName: 'Hopper',
}); });

View File

@@ -1,10 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
export const AlertButton = ({ export const AlertButton = ({}) => {
onSubmit = () => {}, const [message, setMessage] = useState('Alert!');
defaultMessage = 'Alert!',
}) => {
const [message, setMessage] = useState(defaultMessage);
return ( return (
<div> <div>
@@ -17,7 +14,7 @@ export const AlertButton = ({
/> />
</label> </label>
<button onClick={() => onSubmit(message)}>Trigger Alert</button> <button onClick={() => alert(message)}>Trigger Alert</button>
</div> </div>
); );
}; };

View File

@@ -3,28 +3,12 @@ import userEvent from '@testing-library/user-event';
import { AlertButton } from './alert-button'; import { AlertButton } from './alert-button';
describe('AlertButton', () => { describe.todo('AlertButton', () => {
beforeEach(() => {}); beforeEach(() => {});
afterEach(() => {}); afterEach(() => {});
it('should render an alert button', async () => {}); it('should render an alert button', async () => {});
it.only('should trigger an alert', async () => { it('should trigger an alert', async () => {});
const handleSubmit = vi.fn();
render(<AlertButton onSubmit={handleSubmit} message="Default Message" />);
const input = screen.getByLabelText('Message');
const button = screen.getByRole('button', { name: /trigger alert/i });
await act(async () => {
await userEvent.clear(input);
await userEvent.type(input, 'Hello');
await userEvent.click(button);
});
expect(handleSubmit).toHaveBeenCalled();
expect(handleSubmit).toHaveBeenCalledWith('Hello');
});
}); });

View File

@@ -1,26 +1,9 @@
import { screen, fireEvent } from '@testing-library/dom';
import userEvent from '@testing-library/user-event';
import { createButton } from './button.js'; import { createButton } from './button.js';
describe('createButton', () => { describe.todo('createButton', () => {
beforeEach(() => { it('should create a button element', () => {});
document.innerHTML = '';
});
it.skip('should create a button element', () => { it('should have the text "Click Me"', () => {});
document.body.appendChild(createButton());
const button = screen.getByRole('button', { name: 'Click Me' }); it('should change the text to "Clicked!" when clicked', async () => {});
expect(button).toBeInTheDocument();
});
it('should change the text to "Clicked!" when clicked', async () => {
document.body.appendChild(createButton());
const button = screen.getByRole('button', { name: 'Click Me' });
await userEvent.click(button);
expect(button.textContent).toBe('Clicked!');
});
}); });

View File

@@ -9,7 +9,7 @@ describe('Game', () => {
}); });
it('should have a secret number', () => { it('should have a secret number', () => {
// This isn't really a useful test. // Thisn't really a useful test.
// Do I *really* care about the type of the secret number? // Do I *really* care about the type of the secret number?
// Do I *really* care about the name of a "private" property? // Do I *really* care about the name of a "private" property?
const game = new Game(); const game = new Game();

View File

@@ -6,14 +6,11 @@ import { sendToServer } from './send-to-server';
* Log a message to the console in development mode or send it to the server in production mode. * Log a message to the console in development mode or send it to the server in production mode.
* @param {string} message * @param {string} message
*/ */
export function log( export function log(message) {
message, if (import.meta.env.MODE !== 'production') {
{ productionCallback = () => {}, mode = import.meta.env.MODE } = {},
) {
if (mode !== 'production') {
console.log(message); console.log(message);
} else { } else {
productionCallback('info', message); sendToServer('info', message);
} }
} }

View File

@@ -1,33 +1,4 @@
import { expect, it, vi, beforeEach, afterEach, describe } from 'vitest'; import { expect, it, vi, beforeEach, afterEach, describe } from 'vitest';
import { log } from './log'; import { log } from './log';
describe('logger', () => { describe.todo('logger', () => {});
describe('development', () => {
it('logs to the console in development mode', () => {
const logSpy = vi.fn();
log('Hello World');
expect(logSpy).toHaveBeenCalledWith('Hello World');
});
});
describe('production', () => {
beforeEach(() => {
vi.stubEnv('MODE', 'production');
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should not call console.log in production', () => {
const logSpy = vi.spyOn(console, 'log');
log('Hello World', { mode: 'production', productionCallback: logSpy });
expect(logSpy).not.toHaveBeenCalled();
expect(sendToServer).toHaveBeenCalled();
});
});
});

View File

@@ -4,6 +4,5 @@
* @param {string} message * @param {string} message
*/ */
export const sendToServer = (level, message) => { export const sendToServer = (level, message) => {
throw new Error('I should not run!');
return `You must mock this function: sendToServer(${level}, ${message})`; return `You must mock this function: sendToServer(${level}, ${message})`;
}; };

View File

@@ -1,4 +1,6 @@
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; import { vi, describe, it, expect } from 'vitest';
vi.useFakeTimers();
function delay(callback) { function delay(callback) {
setTimeout(() => { setTimeout(() => {
@@ -7,22 +9,5 @@ function delay(callback) {
} }
describe('delay function', () => { describe('delay function', () => {
beforeEach(() => { it.todo('should call callback after delay', () => {});
vi.useFakeTimers();
vi.setSystemTime('2024-02-29');
});
afterEach(() => {
vi.useRealTimers();
});
it('should call callback after delay', () => {
const callback = vi.fn();
delay(callback);
vi.advanceTimersToNextTimer();
expect(callback).toHaveBeenCalled();
expect(new Date()).toBe(null);
});
}); });