Performances, apprentissages et limites
Mise en place de tests d'intégration sur une app React
Par
Joris Langlois
Date
25/03/2025
Conférence

01
Qui suis-je ?

04
Nouvelle approche
Exemple avec MSWjs
02
Introduction
Choix de l'architecture et du cas d'étude
05
Bénéfices et limites
03
Problématique
Limites d'une couverture 100% unitaire. Rapide, simple, mais pour quelle fiabilité ?
06
Conclusion
Mise en place de tests d'intégration sur une app React _ Sommaire
Et questions si vous en avez !
There is no silver bullet
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025
Qui suis-je ?
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025

Qui suis-je ?
En activité depuis 2013
Petit passage par les pays baltes entre
2020 et 2022 (outl1ne)
Chez KNP Labs depuis 2018
Développeur.
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025

Qui suis-je ?
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025
Ecole d'ingénieur du Conservatoire National des Arts et Métiers (EiCNAM) depuis 2017
Certificat de spécialisation : Union Européenne
Auditeur.

Ingéniérie des systèmes decisionnels

Introduction
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025

... par un nouveau projet
un peu trop de certitudes
et un vague sentiment de malaise
Introduction _ Contexte
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025
Tout commença...

Introduction _ Une architecture réactive, qu'est ce que c'est ?
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025


Introduction _ Cas d'étude
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025

Cas 1 : Non authentifié
Cas 2 : En cours d'authentification


Introduction _ Cas d'étude
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025
Cas 3 : Erreur

Cas 4 : Authentification OK


Introduction _ Cas d'étude : vue
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025
// features/Authentication/index.tsx
const Firewall: React.FC<{ children: React.ReactNode[] }> = ({ children }) => {
const dispatch = useDispatch()
const isAuthenticating = useSelector(selectIsAuthenticating)
const isAuthenticated = useSelector(selectIsAuthenticated)
const hasError = useSelector(selectHasError)
if (isAuthenticated) return children
const onSubmit: React.FormEventHandler = e => {
e.preventDefault()
dispatch(slice.actions.authenticateWithCredentials({
username: e.currentTarget.username?.value || '',
password: e.currentTarget.password?.value || '',
}))
}
return <Form onSubmit={ onSubmit }>
{/* ... */}
<SubmitButton loading={ isAuthenticating }>
Se connecter
<Icon name="arrow-right" />
</SubmitButton>
{ hasError && <Message type={ MessageType.ERROR } content="Une erreur s'est produite..." /> }
</Form>
}

Introduction _ Cas d'étude : vue
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025
// features/Authentication/index.tsx
const Firewall: React.FC<{ children: React.ReactNode[] }> = ({ children }) => {
const dispatch = useDispatch()
const isAuthenticating = useSelector(selectIsAuthenticating)
const isAuthenticated = useSelector(selectIsAuthenticated)
const hasError = useSelector(selectHasError)
if (isAuthenticated) return children
const onSubmit: React.FormEventHandler = e => {
e.preventDefault()
dispatch(slice.actions.authenticateWithCredentials({
username: e.currentTarget.username?.value || '',
password: e.currentTarget.password?.value || '',
}))
}
return <Form onSubmit={ onSubmit }>
{/* ... */}
<SubmitButton loading={ isAuthenticating }>
Se connecter
<Icon name="arrow-right" />
</SubmitButton>
{ hasError && <Message type={ MessageType.ERROR } content="Une erreur s'est produite..." /> }
</Form>
}
Couplage fort avec le modèle
=> selecteurs
Introduction _ Cas d'étude : vue
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025
// features/Authentication/index.tsx
const Firewall: React.FC<{ children: React.ReactNode[] }> = ({ children }) => {
const dispatch = useDispatch()
const isAuthenticating = useSelector(selectIsAuthenticating)
const isAuthenticated = useSelector(selectIsAuthenticated)
const hasError = useSelector(selectHasError)
if (isAuthenticated) return children
const onSubmit: React.FormEventHandler = e => {
e.preventDefault()
dispatch(slice.actions.authenticateWithCredentials({
username: e.currentTarget.username?.value || '',
password: e.currentTarget.password?.value || '',
}))
}
return <Form onSubmit={ onSubmit }>
{/* ... */}
<SubmitButton loading={ isAuthenticating }>
Se connecter
<Icon name="arrow-right" />
</SubmitButton>
{ hasError && <Message type={ MessageType.ERROR } content="Une erreur s'est produite..." /> }
</Form>
}
Couplage fort avec le modèle
=> actions
Couplage fort avec le modèle
=> selecteurs
Introduction _ Cas d'étude : modèle
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025
// features/Authentication/slice.ts
export enum Status {
NeedsAuthentication = 'NeedsAuthentication',
Authenticating = 'Authenticating',
Authenticated = 'Authenticated',
UnknownError = 'UnknownError',
}
type State = {
status: Status
}
export const initialState: State = ({
status: Status.NeedsAuthentication,
})
export type AuthenticatePayload = {
username: string,
password: string,
}
export const selectIsAuthenticating = (state: State) =>
state.auth.status === Status.Authenticating
export const selectIsAuthenticated = (state: State) =>
state.auth.status === Status.Authenticated// features/Authentication/slice.ts
const slice = createSlice({
name: 'auth',
initialState,
reducers: {
authenticateWithCredentials: (
state,
_action: PayloadAction<AuthenticatePayload>,
) => {
state.status = AuthStatus.Authenticating,
},
successfullyRetrievedToken: state => state,
authenticated: (
state,
_action: PayloadAction<User>,
) => {
state.status: AuthStatus.Authenticated,
},
error: state => {
state.status: AuthStatus.UnknownError,
},
},
})
Introduction _ Cas d'étude : modèle
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025
// features/Authentication/slice.ts
export enum Status {
NeedsAuthentication = 'NeedsAuthentication',
Authenticating = 'Authenticating',
Authenticated = 'Authenticated',
UnknownError = 'UnknownError',
}
type State = {
status: Status
}
export const initialState: State = ({
status: Status.NeedsAuthentication,
})
export type AuthenticatePayload = {
username: string,
password: string,
}
export const selectIsAuthenticating = (state: State) =>
state.auth.status === Status.Authenticating
export const selectIsAuthenticated = (state: State) =>
state.auth.status === Status.Authenticated// features/Authentication/slice.ts
const slice = createSlice({
name: 'auth',
initialState,
reducers: {
authenticateWithCredentials: (
state,
_action: PayloadAction<AuthenticatePayload>,
) => {
state.status = AuthStatus.Authenticating,
},
successfullyRetrievedToken: state => state,
authenticated: (
state,
_action: PayloadAction<User>,
) => {
state.status: AuthStatus.Authenticated,
},
error: state => {
state.status: AuthStatus.UnknownError,
},
},
})
Introduction _ Cas d'étude : modèle
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025
// features/Authentication/slice.ts
export enum Status {
NeedsAuthentication = 'NeedsAuthentication',
Authenticating = 'Authenticating',
Authenticated = 'Authenticated',
UnknownError = 'UnknownError',
}
type State = {
status: Status
}
export const initialState: State = ({
status: Status.NeedsAuthentication,
})
export type AuthenticatePayload = {
username: string,
password: string,
}
export const selectIsAuthenticating = (state: State) =>
state.auth.status === Status.Authenticating
export const selectIsAuthenticated = (state: State) =>
state.auth.status === Status.Authenticated// features/Authentication/slice.ts
const slice = createSlice({
name: 'auth',
initialState,
reducers: {
authenticateWithCredentials: (
state,
_action: PayloadAction<AuthenticatePayload>,
) => {
state.status = AuthStatus.Authenticating,
},
successfullyRetrievedToken: state => state,
authenticated: (
state,
_action: PayloadAction<User>,
) => {
state.status: AuthStatus.Authenticated,
},
error: state => {
state.status: AuthStatus.UnknownError,
},
},
})
Introduction _ Cas d'étude : effets de bord
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025
// features/Authentication/effects.ts
export default function* rootSaga(): Generator {
yield takeLeading(slice.actions.authenticateWithCredentials, authenticateWithCredentials)
yield takeEvery(slice.actions.successfullyRetrievedToken, getAuthenticatedUser)
}
function* authenticateWithCredentials({ payload }: { payload: AuthenticatePayload }): Generator {
try {
const post = yield getContext(Context.Post)
const container = (yield call(post, '/auth-tokens', payload)) as LdapToken
const storage = yield getContext(Context.Storage)
yield call([ storage, 'setItem' ], 'token', container.token)
yield put(slice.actions.successfullyRetrievedToken())
} catch (e) {
yield put(slice.actions.error())
}
}
function* getAuthenticatedUser(): Generator {
try {
const get = yield getContext(Context.Get)
const user = (yield call(get, '/me')) as User
yield put(slice.actions.authenticated(user))
} catch (e) {
yield put(slice.actions.error())
}
}
Introduction _ Cas d'étude : effets de bord
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025
// features/Authentication/effects.ts
export default function* rootSaga(): Generator {
yield takeLeading(slice.actions.authenticateWithCredentials, authenticateWithCredentials)
yield takeEvery(slice.actions.successfullyRetrievedToken, getAuthenticatedUser)
}
function* authenticateWithCredentials({ payload }: { payload: AuthenticatePayload }): Generator {
try {
const post = yield getContext(Context.Post)
const container = (yield call(post, '/auth-tokens', payload)) as LdapToken
const storage = yield getContext(Context.Storage)
yield call([ storage, 'setItem' ], 'token', container.token)
yield put(slice.actions.successfullyRetrievedToken())
} catch (e) {
yield put(slice.actions.error())
}
}
function* getAuthenticatedUser(): Generator {
try {
const get = yield getContext(Context.Get)
const user = (yield call(get, '/me')) as User
yield put(slice.actions.authenticated(user))
} catch (e) {
yield put(slice.actions.error())
}
}
1. Action in
Introduction _ Cas d'étude : effets de bord
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025
// features/Authentication/effects.ts
export default function* rootSaga(): Generator {
yield takeLeading(slice.actions.authenticateWithCredentials, authenticateWithCredentials)
yield takeEvery(slice.actions.successfullyRetrievedToken, getAuthenticatedUser)
}
function* authenticateWithCredentials({ payload }: { payload: AuthenticatePayload }): Generator {
try {
const post = yield getContext(Context.Post)
const container = (yield call(post, '/auth-tokens', payload)) as LdapToken
const storage = yield getContext(Context.Storage)
yield call([ storage, 'setItem' ], 'token', container.token)
yield put(slice.actions.successfullyRetrievedToken())
} catch (e) {
yield put(slice.actions.error())
}
}
function* getAuthenticatedUser(): Generator {
try {
const get = yield getContext(Context.Get)
const user = (yield call(get, '/me')) as User
yield put(slice.actions.authenticated(user))
} catch (e) {
yield put(slice.actions.error())
}
}
1. Action in
2. Effet
Introduction _ Cas d'étude : effets de bord
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025
// features/Authentication/effects.ts
export default function* rootSaga(): Generator {
yield takeLeading(slice.actions.authenticateWithCredentials, authenticateWithCredentials)
yield takeEvery(slice.actions.successfullyRetrievedToken, getAuthenticatedUser)
}
function* authenticateWithCredentials({ payload }: { payload: AuthenticatePayload }): Generator {
try {
const post = yield getContext(Context.Post)
const container = (yield call(post, '/auth-tokens', payload)) as LdapToken
const storage = yield getContext(Context.Storage)
yield call([ storage, 'setItem' ], 'token', container.token)
yield put(slice.actions.successfullyRetrievedToken())
} catch (e) {
yield put(slice.actions.error())
}
}
function* getAuthenticatedUser(): Generator {
try {
const get = yield getContext(Context.Get)
const user = (yield call(get, '/me')) as User
yield put(slice.actions.authenticated(user))
} catch (e) {
yield put(slice.actions.error())
}
}
1. Action in
2. Effet
3. Action out !
Introduction _ Cas d'étude : effets de bord
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025

// features/Authentication/effects.ts
export default function* rootSaga(): Generator {
yield takeLeading(slice.actions.authenticateWithCredentials, authenticateWithCredentials)
yield takeEvery(slice.actions.successfullyRetrievedToken, getAuthenticatedUser)
}
function* authenticateWithCredentials({ payload }: { payload: AuthenticatePayload }): Generator {
try {
const post = yield getContext(Context.Post)
const container = (yield call(post, '/auth-tokens', payload)) as LdapToken
const storage = yield getContext(Context.Storage)
yield call([ storage, 'setItem' ], 'token', container.token)
yield put(slice.actions.successfullyRetrievedToken())
} catch (e) {
yield put(slice.actions.error())
}
}
function* getAuthenticatedUser(): Generator {
try {
const get = yield getContext(Context.Get)
const user = (yield call(get, '/me')) as User
yield put(slice.actions.authenticated(user))
} catch (e) {
yield put(slice.actions.error())
}
}4. Action in
Introduction _ Cas d'étude : effets de bord
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025

// features/Authentication/effects.ts
export default function* rootSaga(): Generator {
yield takeLeading(slice.actions.authenticateWithCredentials, authenticateWithCredentials)
yield takeEvery(slice.actions.successfullyRetrievedToken, getAuthenticatedUser)
}
function* authenticateWithCredentials({ payload }: { payload: AuthenticatePayload }): Generator {
try {
const post = yield getContext(Context.Post)
const container = (yield call(post, '/auth-tokens', payload)) as LdapToken
const storage = yield getContext(Context.Storage)
yield call([ storage, 'setItem' ], 'token', container.token)
yield put(slice.actions.successfullyRetrievedToken())
} catch (e) {
yield put(slice.actions.error())
}
}
function* getAuthenticatedUser(): Generator {
try {
const get = yield getContext(Context.Get)
const user = (yield call(get, '/me')) as User
yield put(slice.actions.authenticated(user))
} catch (e) {
yield put(slice.actions.error())
}
}4. Action in
5. Effet
Introduction _ Cas d'étude : effets de bord
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025

// features/Authentication/effects.ts
export default function* rootSaga(): Generator {
yield takeLeading(slice.actions.authenticateWithCredentials, authenticateWithCredentials)
yield takeEvery(slice.actions.successfullyRetrievedToken, getAuthenticatedUser)
}
function* authenticateWithCredentials({ payload }: { payload: AuthenticatePayload }): Generator {
try {
const post = yield getContext(Context.Post)
const container = (yield call(post, '/auth-tokens', payload)) as LdapToken
const storage = yield getContext(Context.Storage)
yield call([ storage, 'setItem' ], 'token', container.token)
yield put(slice.actions.successfullyRetrievedToken())
} catch (e) {
yield put(slice.actions.error())
}
}
function* getAuthenticatedUser(): Generator {
try {
const get = yield getContext(Context.Get)
const user = (yield call(get, '/me')) as User
yield put(slice.actions.authenticated(user))
} catch (e) {
yield put(slice.actions.error())
}
}4. Action in
5. Effet
6. Action out !
Introduction _ Cas d'étude : modèle
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025
// features/Authentication/slice.ts
export enum Status {
NeedsAuthentication = 'NeedsAuthentication',
Authenticating = 'Authenticating',
Authenticated = 'Authenticated',
UnknownError = 'UnknownError',
}
type State = {
status: Status
}
export const initialState: State = ({
status: Status.NeedsAuthentication,
})
export type AuthenticatePayload = {
username: string,
password: string,
}
export const selectIsAuthenticating = (state: State) =>
state.auth.status === Status.Authenticating
export const selectIsAuthenticated = (state: State) =>
state.auth.status === Status.Authenticated// features/Authentication/slice.ts
const slice = createSlice({
name: 'auth',
initialState,
reducers: {
authenticateWithCredentials: (
state,
_action: PayloadAction<AuthenticatePayload>,
) => {
state.status = AuthStatus.Authenticating,
},
successfullyRetrievedToken: state => state,
authenticated: (
state,
_action: PayloadAction<User>,
) => {
state.status: AuthStatus.Authenticated,
},
error: state => {
state.status: AuthStatus.UnknownError,
},
},
})
Problématique
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025

Problématique _ Fonctions
Qu'il s'agisse de la vue :
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025
Du modèle :
Ou des effets de bord :
Des fonctions, des fonctions, toujours des fonctions !

Et nos tests unitaires, dans tout ça ?
- Modèle : application directe et triviale
- Effets de bord : possible, à un certain coût
- Vues : de quels composants parle-t-on ?
Quelques propriétés élémentaires que l'on peut exploiter à notre avantage :
- Pureté
- Composabilité
Dont découle logiquement une approche fonctionnelle des tests :
- La robustesse du système est garantie par la composition de fonctions pures unitairement testées
Problématique _ Fonctions
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025

Tests de selecteurs (modèle)
- Certains sélecteurs ne sont que des accesseurs
- Chronophages, verbeux et répétitifs
Tests d'effets de bord (appels d'API, accès au presse papier, au local storage, ...)
- Nécessitent de mocker le comportement des dépendances injectées
- 90% des effets de bord ne gèrent "que" des appels d'API (succès / erreur)
- Verbeux et répétitifs
- Peu lisibles / maintenables (marble testing)
Problématique _ Limites de l'approche unitaire
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025

Tests de reducers (modèle)
- Incontournables
Tests de composants d'interface
- Réutilisables par définition
- Gèrent leur propre état interne
- Peu couplés aux autres couches
- Composables
Problématique _ Limites de l'approche unitaire
Tests de composants montés par une route (features)
- Spécifiques, peu réutilisables et composables
- Fortement couplés aux autres couches
- Impliquent un certain degré de simulation pour pouvoir mettre ces composants dans l'état souhaité pour pouvoir les tester
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025

Problématique _ Limites de l'approche unitaire : exemple avec React Testing Library
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025



// features/Authentication/Firewall.test.tsx
describe('features/Me/Authentication', () => {
test('authenticates', async () => {
const { userEvent } = setup(<Firewall>app</Firewall>)
const submit = screen.getByRole('button', { name: 'Se connecter' })
expect(submit).toBeEnabled()
await userEvent.type(screen.getByLabelText('Email'), 'joris.langlois@knplabs.com')
await userEvent.type(screen.getByLabelText('Mot de passe'), 'notanychance')
await userEvent.click(submit)
expect(submit).toBeDisabled()
act(() => {
store.dispatch(slice.actions.authenticated({
firstname: 'Joris',
lastname: 'Langlois',
company: 'KNP Labs',
// ...
}))
})
expect(await screen.findByText(/app/)).toBeInTheDocument()
})
})
// features/Authentication/Firewall.test.tsx
describe('features/Me/Authentication', () => {
test('authenticates', async () => {
const { userEvent } = setup(<Firewall>app</Firewall>)
const submit = screen.getByRole('button', { name: 'Se connecter' })
expect(submit).toBeEnabled()
await userEvent.type(screen.getByLabelText('Email'), 'joris.langlois@knplabs.com')
await userEvent.type(screen.getByLabelText('Mot de passe'), 'notanychance')
await userEvent.click(submit)
expect(submit).toBeDisabled()
act(() => {
store.dispatch(slice.actions.authenticated({
firstname: 'Joris',
lastname: 'Langlois',
company: 'KNP Labs',
// ...
}))
})
expect(await screen.findByText(/app/)).toBeInTheDocument()
})
})Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025




Problématique _ Limites de l'approche unitaire : exemple avec React Testing Library
Risques de faux positifs
- Si on supprime / modifie l'effet responsable de diffuser les actions (authenticated ou error) dans l'application, les tests passent toujours, mais l'application est KO
Risques de faux négatifs
- Si on arrête de diffuser ces actions dans les tests, ils sont KO alors que l'application va très bien...
Problématique _ Bilan
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025
Un bilan mi-figue mi-raisin
- Les composants fonctionnent tous en isolation mais...
- Les conditions d'exécution sont éloignées de la réalité
- On teste l'implémentation plutôt que le comportement
- Tests chronophages, répétitifs et verbeux
- Pour un gain de confiance faible

Nouvelle approche
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025

It's all about getting a good return on your investment where "return" is confidence and "investment" is time.
Kent C. Dodds - 2021
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025
Nouvelle approche _ Quelle stratégie ?

Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025
Nouvelle approche _ Quelle stratégie ?

Kent C. Dodds. Write tests. Not too many. Mostly integration. 2019.
- https://kentcdodds.com/blog/write-tests

Principe
- Plutôt que de tout tester de la même manière et de façon peu efficace, on déplace l'effort de test pour le centrer sur le comportement de l'utilisateur
Scénario nominal
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025
Nouvelle approche _ Exemple avec MSWjs
// features/Authentication/Firewall.test.tsx
describe('features/Me/Authentication', () => {
test('authenticates', async () => {
const { userEvent } = setup(<Firewall>app</Firewall>)
const submit = screen.getByRole('button', { name: 'Se connecter' })
expect(submit).toBeEnabled()
await userEvent.type(screen.getByLabelText('Email'), 'joris.langlois@knplabs.com')
await userEvent.type(screen.getByLabelText('Mot de passe'), 'notanychance')
await userEvent.click(submit)
expect(submit).toBeDisabled()
expect(await screen.findByText(/app/)).toBeInTheDocument()
})
})



MSW (Mock Service Worker), par Artem Zakharchenko et 150+ contributeurs
- Interception bas niveau des appels réseaux
- Agnostique du client
- Tous les composants, de l'envoi de la requête à sa résolution, sont exécutés
Nouvelle approche _ Exemple avec MSWjs
// setupTests.ts
import { setupServer } from 'msw/node'
import { handlers } from 'test/mocks/handlers'
const server = setupServer(...handlers)
beforeAll(() => {
server.listen()
})
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025
// test/mocks/handlers.ts
import { HttpResponse, http } from 'msw'
import { users } from 'test/fixtures'
const ok = (body:any = null) => new HttpResponse(
JSON.stringify(body),
{
status: 200,
headers: { 'Content-Type': ContentType.Json },
}
)
const handlers = [
http.post('/auth-tokens', () => ok({ token: 'my-token' })),
http.get('/me', () => ok(users[0])),
]
Scénario d'échec
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025
Nouvelle approche _ Exemple avec MSWjs
// features/Authentication/Firewall.test.tsx
describe('features/Me/Authentication', () => {
test('cannot authenticate', async () => {
const { userEvent } = setup(<Firewall />)
const submit = screen.getByRole('button', { name: 'Se connecter' })
server.use(
http.get('/me', async () => HttpResponse.error())
)
await userEvent.type(screen.getByLabelText('Email'), 'joris.langlois@knplabs.com')
await userEvent.type(screen.getByLabelText('Mot de passe'), 'notanychance')
await userEvent.click(submit)
await waitFor(() => expect(submit).toBeDisabled())
await waitFor(() => expect(submit).toBeEnabled())
expect(screen.getByText(/Une erreur s'est produite/)).toBeInTheDocument()
})
})



Bénéfices et limites
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025

Les sélecteurs de type accesseurs sont testés indirectement
- Plus besoin de les tester unitairement
La plupart des effets de bord sont aussi testés indirectement à partir du moment où l'interface change en réaction à une action système
- Plus besoin de les tester unitairement ni même de les exporter (= simplification)
Plus besoin de diffuser des actions "à la main" dans les tests de composant pour simuler un comportement
- Tests plus lisibles et plus simples à maintenir
Baisse du risque de faux positifs / négatifs
- Davantage de confiance dans ce qu'on livre
- Diminution du risque de régression
Bénéfices et limites _ Bénéfices
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025

Risque de tests un peu instables lors de certains enchaînements
Prise en main : tout est asynchrone !
- Résolution des réponses de MSW potentiellement instantanées
- Besoin d'attendre que les éléments de l'interface soient tous là pour les assertions et passer au test suivant
Plus lents que des tests unitaires
- (100 suites / 500 tests ~ 50s)
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025
Bénéfices et limites _ Limites

Bénéfices et limites _ Réflexions complémentaires
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025
// apps/frontend/test/fixtures/index.ts
export const users = [
{
id: 1,
firstname: 'John',
lastname: 'Doe',
profilePicturePath: 'cat.png',
locale: 'fr',
role: 'admin',
},
]// apps/backend/doc/schemas/user.json
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "http://cacom.fr/user.json",
"type": "object",
"required": [ "id", "firstname", "lastname", "locale" ],
"additionalProperties": false,
"properties": {
"id": {
"type": "integer"
},
"firstname": {
"type": "string"
},
"lastname": {
"type": "string"
},
"profilePicturePath": {
"type": "string"
},
"locale": {
"type": "string"
},
"role": {
"$ref": "defs/user-role.json"
}
}
}
Si les jeux de test (fixtures) sont :
- testés unitairement en utilisant les JSON schémas du backend...
- ...tout comme les données que l'on envoie à l'API

Les tests E2E sont-ils utiles ?
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025

Quel coverage ?
Bénéfices et limites _ Réflexions complémentaires

(c) Google 2024

Adam Bender. SMURF: Beyond the Test Pyramid. 2024.
- https://testing.googleblog.com/2024/10/smurf-beyond-test-pyramid.html
Mise en place de tests d'intégration sur une app React - PTC 25 mars 2025
Merci !

PTC - 25 mars 2025
By KNP Labs
PTC - 25 mars 2025
- 5