Software engineer - Freelancer
Manual tests
Feature code
Tools
Spend more time on debugging than coding
Have to code the whole feature to test/try it
I have to chose my tools before starting coding
System/End-to-end testing
Integration/Contract testing
Unit testing
Load testing
Performance testing
Manual testing
Unit tests are automated tests written and run by software developers to ensure that a section of an application (known as the "unit") behaves as intended
Wikipedia
Integration tests
End to End tests
Unit tests
Fast
Slow
Dev
Regression
Feature code
Tools (IO)
Unit
Test
Unit
Test
Unit
Test
Unit
Test
Unit
Test
Feature code
Ease code refactoring
Ease application evolution
Work on small pieces of code
Le sunset
describe('Map', () => {
it('adds a new marker to the map', () => {
// Arrange
const map = new Map(new Name('Anglet'), MarkerList.empty())
// Act
map.addMarker('Le Sunset', 23.252353245, 43.5432563457)
// Assert
expect(map).toEqual(
new Map(
new Name('Anglet'),
[new Marker('Le Sunset', 23.252353245, 43.5432563457)]
)
)
})
})
class MyClass {
myMethod() {
const repository = PosgresqlRepository.getInstance()
repository.save(new MyObject())
}
}
class MyClass {
constructor(private repository: PosgresqlRepository) {}
myMethod() {
this.repository.save(new MyObject())
}
}
Concrete
class
Concrete
class
Abstraction
depends on
Input / Ouput
Code
class AddMarkerToMap {
execute(marker) {
const repository = PosgresqlMaps.getInstance()
const map = repository.get(marker.mapId)
map.addMarker(
marker.name,
marker.longitude,
marker.latitude,
)
repository.save(map)
}
}
interface Maps {
get(mapId: MapId): Map
save(map: Map)
}
// Test
class InMemoryMaps implements Maps {
// Keep map objects in memory
}
// Production
class PosgresqlMaps implements Maps {
// Use Posgresql connection
}
class AddMarkerToMap {
constructor(private maps: Maps) {}
execute(marker) {
const map = this.maps.get(marker.mapId)
map.addMarker(
marker.name, marker.latitude, marker.longitude
)
this.maps.save(map)
}
}
it('adds a new marker to the map', () => {
const maps = InMemoryMaps()
new AddMarkerToMap(maps).execute({
mapId: 'mapId', name: 'Le Sunset',
latitude: 23.252353245, longitude: 43.5432563457
})
expect(maps.get('mapId')).toEqual(
new Map(
new Marker('Le Sunset', 23.252353245, 43.5432563457)
)
)
})
Domain (no IO)
Infrastructure (IO)
User
interface
Adapter
Port
Port
Adapter
Port
Web
Port
CLI
Domain (no IO)
User interface
Primary port
Port
Web
CLI
Infrastructure (IO)
Secondary port
Adapter
Port
Adapter
Domain (no IO)
Infrastructure (IO)
Maps
PosgresqlMaps
InMemoryMaps
Hexagonal architecture
schéma port adapter + montrer comment on teste ça après
Domain (no IO)
Unit tests
Infrastructure (IO)
Integration tests
Concrete
class
Interface
Interface
Concrete
class
Public method
Private method
Private method
Public method
Public method
Class
Test
Public method
Public method
Class
Private method
Private method
Private method
Private method
Private method
Class
A
B
C
F
E
D
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
B
A
C
D
E
F
A
B
C
F
E
D
class Account {
constructor(private username: string, private password: string) {
this.hash = bcrypt.hashSync(password, 10)
}
}
class RegisterCartographer {
constructor(private accounts: Accounts) {}
execute(account) {
this.accounts.save(
new Account(account.username, account.password)
)
}
}
it('creates a cartographer account', () => {
const accounts = InMemoryAccounts()
new RegisterCartographer(accounts).execute({
usename: 'Pepito', password: 'password1'
})
// This test fails!
expect(accounts.getByUsername('Pepito')).toEqual(
new Account('Pepito', 'password1')
)
})
interface PasswordEncryptor {
hash(password: string): string
}
// Test
class InMemoryPasswordEncryptor implements PasswordEncryptor {
hash(password: string): string {
return '$2y$10$JqfiXNdcuWErfiy5pAJ4O.wK(...)';
}
}
// Production
class BcryptPasswordEncryptor implements PasswordEncryptor {
hash(password: string): string {
return bcrypt.hashSync(password, 10);
}
}
class Account {
constructor(
private username: string, private hashedPassword: string
) {}
}
class RegisterCartographer {
constructor(
private accounts: Accounts,
private passwordEncryptor: PasswordEncryptor
)
execute(account) {
this.accounts.save(new Account(
account.username,
this.passwordEncryptor.hash(account.password)
))
}
}
class AddMarkerToMap {
constructor(private maps: Maps) {}
execute(marker) {
const map = this.maps.get(marker.mapId)
map.addMarker(
marker.name, marker.latitude, marker.longitude,
new Date()
)
this.maps.save(map)
}
}
it('adds a new marker to the map', () => {
const maps = InMemoryMaps()
new AddMarkerToMap(maps).execute({
mapId: 'mapId', name: 'Le Sunset',
latitude: 23.252353245, longitude: 43.5432563457
})
// This test fails!
expect(maps.get('mapId')).toEqual(
new Map(new Marker(
'Le Sunset', 23.252353245, 43.5432563457, new Date()
))
)
})
interface Clock {
now(): Date
}
// Test
class InMemoryClock implements Clock {
now(): Date {
return new Date('2022-04-13')
}
}
// Production
class SystemClock implements Clock {
now(): Date {
return new Date()
}
}
class AddMarkerToMap {
constructor(private clock: Clock) {}
execute(marker): void {
// ...
map.addMarker(
marker.name,
marker.longitude,
marker.latitude,
this.clock.now()
)
// ...
}
}
Public method
Public method
Class
Public method
Constructor
Internal
State
Getters
findClosestMarker
getMarkers
it('rename a map', () => {
const map = new Map(
new Name('Anglette city'), MarkerList.empty()
)
map.rename('Anglet city');
expect(map.getName()).toEqual(new Name('Anglet city'))
})
it('renames a map', () => {
const map = new Map(
new Name('Anglette city'), MarkerList.empty()
)
map.rename('Anglet city');
expect(map).toEqual(
new Map(new Name('Anglet city'), MarkerList.empty())
)
// OR
expect(
map.equals(
new Map(new Name('Anglet city'), MarkerList.empty())
)).toBe(true)
})
describe('Map', () => {
// Which business rule do you want to describe?
it('should not add a marker', () => {
// ...
})
// this test case is much more clearer
it('should not add a marker if a marker has the same location', () => {
// ...
})
})
describe('Map', () => {
// What happens if the method signature change?
it('returns the Location type of the closest marker', () => {
// ...
})
// What happens if the method implementation change?
it('calls the the method Repository.save method', () => {
// ...
})
})
it('renames a map', () => {
const map = new Map(
new MapId('e9a01a8a-9d40-476e-a946-06b159cd484a'),
new Cartographer('Pepito'),
new Name('Anglette city'),
new Description('Good places in Anglet'),
MarkerList.empty(),
);
map.rename('Anglet city');
expect(map).toEqual(new Map(
new MapId('e9a01a8a-9d40-476e-a946-06b159cd484a'),
new Cartographer('Pepito'),
new Name('Anglet city'),
new Description('Good places in Anglet'),
MarkerList.empty(),
))
})
class Map {
static whatever(map: Partial<{
mapId: string,
addedBy: string,
name: string,
description: string,
markers: Marker[],
}>): Map {
return new Map(
new MapId(map.mapId ?? 'e9a01a8a-9d40-476e-a946-06b159cd484a'),
new Cartographer(map.addedBy ?? 'Pepito'),
new Name(map.name ?? 'Anglet city'),
new Description(map.description ?? 'Good places in Anglet'),
new MarkerList(map.markers ?? []),
)
}
}
it('renames a map', () => {
const map = Map.whatever({name: 'Anglette city'})
map.rename('Anglet city');
expect(map).toEqual(Map.whatever({name: 'Anglet city'}))
})
class Map {
/** @internal use for testing purpose */
static whatever(map: Partial<{
mapId: string,
addedBy: string,
name: string,
description: string,
markers: Marker[],
}>): Map {
return new Map(
// ...
)
}
}
class aMap
{
private mapId: string = 'e9a01a8a-9d40-476e-a946-06b159cd484a'
private name: string = 'Anglet city'
// initialize other properties
named(name: string): MapBuilder {
this.name = name
return this
}
// ... other setters ....
build(): Map {
return new Map(
new MapId(this.mapId), new MapName(this.name), /** etc... */
)
}
}
it('renames a map', () => {
const map = new aMap()
.named('Anglette city')
.build();
map.rename('Anglet city');
expect(map).toEqual(
new aMap()
.named('Anglet city')
.build()
)
})
End to End tests
Unit tests
Fast
Slow
Make your tests clear and understandable
Make your tests easy to write (builders, helpers)
Do not hesitate to remove unrelevant tests
it('finds the latest added marker', () => {
const marker = Marker.whatever({
makerId: 'a7bda4a5-8f79-41d6-92e6-f0bb2197d086'
})
const map = Map.whatever({markers: [marker]})
expect(map.latestAddedMarker()).toEqual(marker)
})
Red
Green
Refactor