Summary
Это функция, которая создаёт объект определённого типа с уже установленными валидными значениями по-умолчанию. И предоставляет возможность вручную определить необходимые для работы параметры на выбор.
Введение
Хочу рассказать про штуку, которую считаю одной из полезнейших, что я внедрил в проекты Ozon Эквайринга за последний год. Это небольшой, но очень важный шаг к приложению, которое можно просто и быстро протестировать
Проблема
Когда разработчик пишет юнит тест, часто получается так, что входной параметр тестируемой функции это объект, у которого есть данные, которые обязательно должны быть определены. Даже если они не нужны для конкретного теста.
Приходится каждый раз определять начальные значения для каждого юнита. Выглядит это примерно так:
describe("ChangeUserEmail (Изменить почту пользователя)", () => {
const initialUserState: User = {
firstName: "",
lastName: "",
email: "mr.anderson@gmail.com",
phone: "",
}
const state = userReducer(initialUserState, new ChangeUserEmail("neo@gmail.com"))
test("В бизнес стейте обновляется поле email", () => {
expect(state.email).toBe("neo@gmail.com")
})
})
- В следующем тесте на изменение имени, телефона и т.п. придётся создать такой же объект, где нужно подставить только одну строку;
- Если объект сложный и состоит из множества объектов, то воспроизвести нужно и их.
Решение
Решением этой проблемы могут быть фабрики значений по-умолчанию. Для того, чтобы начать их использовать, нужно добавить в файл к типу функцию с таким же именем, просто в camelCase:
export interface User {
firstName: string
lastName: string
email: string
phone: string
}
export const user = (data: Partial<User> = {}): User => ({
firstName: "",
lastName: "",
email: "",
phone: "",
...data,
})
Всем полям присваиваются значения по умолчанию. Через параметр функции передаётся переменная data: Partial<User>
с переопределением некоторых полей, которые пользователь кода захочет изменить.
Если нужно будет добавить новое поле в тип, то с фабрикой это легко сделать, не сломав ничего в множестве тестов, где он используется. Линтер сразу же начнёт ругаться: “Добавь в фабрику дефолтное поле”.
Результат
- Код теста упрощается
- Уменьшается количество визуального мусора
- При добавлении новых полей в класс, не ломаются уже написанные тесты
describe("ChangeUserEmail (Изменить почту пользователя)", () => {
const state = userReducer(
user({ email: "mr.anderson@gmail.com" }),
new ChangeUserEmail("neo@gmail.com"),
)
test("В бизнес стейте обновляется поле email", () => {
expect(state.email).toBe("neo@gmail.com")
})
})
Типы и их дефолтные значения
string
-""
number
-0
boolean
-false
Array<any>
-[]
Object какого-то типа
-фабрика этого типа
Любое опциональное значение
-undefined
Некоторые мысли
- На один тип лучше делать один файл;
- Тип и фабрика должны лежать в одном файле, так их легче поддерживать;
- Фабрику лучше называть так же, как сам тип, просто с маленькой буквы. Это позволяет быстро находить нужную фабрику и не допускает конфликта имён;
- Фабрики типа можно использовать для дефолтного стейта в сторе Redux, Vuex;
- Если есть желание использовать фабрики повсюду в проекте, то лучше этого избегать, и стараться формировать тип так, чтобы он содержал в себе только то, что будет действительно использоваться. У моего друга Android-разработчика в проекте вообще стоит декоратор
@testOnly
над фабриками дефолтных значений.
Тэги
Вдохновляют
- @shevtsov200
- Супрематисты, но эти не то чтобы сильно
Материалы
- Владимир Хориков: Принципы юнит тестирования - В разделе 3.3.3 он использует похожие фабрики
- James Shore: Testing Without Mocks: A Pattern Language - Джеймс Шор называет это Parameterless Instantiation и добавляет туда уже ненулевые значения, но суть та же