Тестирование
@vue-modeler/model позволяет писать меньше кода, это значит нужно меньше тестов. Вам не нужно проверять правильность установки состояний в action, если они не используются во внутренней бизнес логике модели.
Юнит-тестирование
- не требует дополнительных инструментов: библиотек, плагинов, и т.п.
- не отличается от тестирования экземпляров обычных классов.
Для каждого теста нужно:
- Создать моки на зависимости
- Создать модель
- Выполнить тест
Основные моменты:
- Модели создаются через статический метод
model - Действия — это объекты класса
Action, выполняются черезexec() - Если бизнес-логика не завязана на состояние или ошибки
action, то не нужно проверять правильность установки состояния вaction - Зависимости легко мокать через фабричные функции и внедрять через конструктор модели
- модели — реактивные объекты, поэтому нужно понимать особенности тестирования реактивных объектов
Следуя этим практикам, вы сможете писать надежные и поддерживаемые тесты для ваших моделей.
Пример
Для демонстрации возьмем пример близкий к реальности — Корзина товаров. Корзина будет максимально простой:
- работает только для аутентифицированного пользователя
- хранит только sku без количества
- автоматически синхронизируется при добавлении или удалении товара
typescript
// examples/cart.ts
import { ProtoModel, action } from '@vue-modeler/model'
import { ShallowReactive } from 'vue'
interface ApiService {
fetchAll: () => Promise<string[]>
add: (sku: string) => Promise<string[]>
remove: (sku: string) => Promise<string[]>
}
interface User {
isLoggedIn: boolean
}
export class Cart extends ProtoModel {
protected _items: Set<string> = new Set()
constructor(
// снаружи класса user может быть моделью.
// Так как модель — это ShallowReactive объект, то
// он будет совместим с ShallowReactive<User>
private user: ShallowReactive<User> ,
private api: ApiService,
) {
super()
this.watch(
() => this.user.isLoggedIn,
async (isLoggedIn: boolean) => {
// если состояние пользователя изменилось,
// а действие init всё еще выполняется — отменяем его
if (this.action(this.init).isPending) {
await this.action(this.init).abort()
}
// если user уже вошёл, запускаем init и выходим.
// Ждать init не обязательно. Если состояние пользователя изменится,
// и init будет выполняться — он отменится выше
if (isLoggedIn) {
this.init()
return
}
// user вышел, сбрасываем корзину.
this._items = new Set()
},
{ immediate: true },
)
}
get items(): Set<string> {
return this._items
}
@action async init(): Promise<void> {
const res = await this.api.fetchAll()
this._items = new Set(res)
}
@action async add(sku: string): Promise<void> {
const res = await this.api.add(sku)
this._items = new Set(res)
}
@action async remove(sku: string): Promise<void> {
const res = await this.api.remove(sku)
this._items = new Set(res)
}
}Здесь приведем только тест на более сложный случай: создание корзины -> логин пользователя -> разлогин -> повторный логин.
Полный набор тестов доступен здесь.
typescript
import { nextTick } from 'vue'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { shallowReactive, ShallowReactive } from 'vue'
import { Cart } from '../cart'
import type { Model } from '@vue-modeler/model'
interface ApiService {
fetchAll: () => Promise<string[]>
add: (sku: string) => Promise<string[]>
remove: (sku: string) => Promise<string[]>
}
interface User {
isLoggedIn: boolean
}
describe('Cart', () => {
let apiService: ApiService
let user: ShallowReactive<User>
let cart: Model<Cart>
beforeEach(() => {
apiService = {
fetchAll: vi.fn(),
add: vi.fn(),
remove: vi.fn(),
}
user = shallowReactive<User>({
isLoggedIn: false,
})
cart = Cart.model(user, apiService)
})
...
describe('watch behavior', () => {
...
it('watches user login state and re-initializes items when user logs back in after logout', async () => {
const firstItems = ['item1', 'item2']
const secondItems = ['item3', 'item4', 'item5']
vi.mocked(apiService.fetchAll)
.mockResolvedValueOnce(firstItems)
.mockResolvedValueOnce(secondItems)
// First login
user.isLoggedIn = true
await nextTick()
expect(cart.items.size).toBe(2)
// Log out
user.isLoggedIn = false
await nextTick()
expect(cart.items.size).toBe(0)
// Log in again
user.isLoggedIn = true
await nextTick()
expect(cart.items.size).toBe(3)
expect(Array.from(cart.items)).toEqual(secondItems)
expect(apiService.fetchAll).toHaveBeenCalledTimes(2)
})
})
})Полный список тестов для такой корзины можно посмотреть здесь.
