Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Привязка к транспорту в тестах (Transport Coupling)
Отличный архитектурный вопрос! Это ключевая проблема при разработке тестируемого кода. Расскажу об этом паттерне и как его избежать.
Что такое Transport Coupling?
Transport Coupling - это зависимость кода от способа передачи данных (HTTP, WebSocket, gRPC, и т.д.). Если ваш тест привязан к конкретному транспорту, он сложнее писать и поддерживать.
Плохой пример - с Transport Coupling
// UserService - привязан к HTTP
class UserService {
async getUser(id) {
// Прямой вызов fetch - транспорт очень связан
const response = await fetch(`/api/v1/users/${id}`);
return response.json();
}
}
// Тест - должен мокировать fetch
test("getUser returns user data", async () => {
// Мокируем глобальный fetch - это хрупко
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ id: 1, name: "John" })
})
);
const service = new UserService();
const user = await service.getUser(1);
expect(user.name).toBe("John");
expect(global.fetch).toHaveBeenCalledWith("/api/v1/users/1");
});
Проблемы:
- Тест зависит от HTTP и fetch API
- Если поменять на axios или fetch wrapper - тест сломается
- Нужно мокировать глобальные объекты
- Сложно тестировать с разными транспортами
Хороший пример - без Transport Coupling
// Создаем абстракцию для HTTP клиента
interface HttpClient {
get<T>(url: string): Promise<T>;
post<T>(url: string, data: any): Promise<T>;
}
// UserService зависит от абстракции, не от конкретного транспорта
class UserService {
constructor(private httpClient: HttpClient) {}
async getUser(id: number) {
return this.httpClient.get(`/api/v1/users/${id}`);
}
}
// Реальная имплементация - с fetch
class FetchHttpClient implements HttpClient {
async get<T>(url: string): Promise<T> {
const response = await fetch(url);
return response.json();
}
async post<T>(url: string, data: any): Promise<T> {
const response = await fetch(url, {
method: "POST",
body: JSON.stringify(data)
});
return response.json();
}
}
// Тест - использует mock вместо реального транспорта
class MockHttpClient implements HttpClient {
async get<T>(url: string): Promise<T> {
// Возвращаем тестовые данные
if (url === "/api/v1/users/1") {
return { id: 1, name: "John" } as T;
}
throw new Error("Not found");
}
async post<T>(url: string, data: any): Promise<T> {
return { ...data, id: 1 } as T;
}
}
// Тест - не привязан к транспорту
test("getUser returns user data", async () => {
const mockClient = new MockHttpClient();
const service = new UserService(mockClient);
const user = await service.getUser(1);
expect(user.name).toBe("John");
});
// Когда нужно поменять транспорт - просто создаем новую имплементацию
class AxiosHttpClient implements HttpClient {
constructor(private axiosInstance = axios) {}
async get<T>(url: string): Promise<T> {
const response = await this.axiosInstance.get(url);
return response.data;
}
async post<T>(url: string, data: any): Promise<T> {
const response = await this.axiosInstance.post(url, data);
return response.data;
}
}
// UserService продолжает работать - тесты не меняются!
Dependency Injection - решение
Железный принцип: Inject dependencies, don't create them
// Плохо - создает зависимость внутри
class UserRepository {
private httpClient = new FetchHttpClient();
async getUser(id: number) {
return this.httpClient.get(`/api/v1/users/${id}`);
}
}
// Хорошо - получает зависимость извне
class UserRepository {
constructor(private httpClient: HttpClient) {}
async getUser(id: number) {
return this.httpClient.get(`/api/v1/users/${id}`);
}
}
// Использование
const realClient = new FetchHttpClient();
const userRepository = new UserRepository(realClient);
// В тестах
const mockClient = new MockHttpClient();
const userRepositoryTest = new UserRepository(mockClient);
Frontend пример - с React Query
// Плохо - компонент привязан к fetch
function UserProfile({ userId }) {
const [user, setUser] = React.useState(null);
React.useEffect(() => {
fetch(`/api/v1/users/${userId}`)
.then(r => r.json())
.then(setUser);
}, [userId]);
return <div>{user?.name}</div>;
}
// Хорошо - используем React Query (обеспечивает слой абстракции)
import { useQuery } from "@tanstack/react-query";
function useUser(userId: number) {
return useQuery({
queryKey: ["users", userId],
queryFn: async () => {
const response = await fetch(`/api/v1/users/${userId}`);
return response.json();
}
});
}
function UserProfile({ userId }) {
const { data: user } = useUser(userId);
return <div>{user?.name}</div>;
}
// Тест - мокируем React Query, не fetch
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, screen } from "@testing-library/react";
test("UserProfile displays user name", () => {
const queryClient = new QueryClient();
queryClient.setQueryData(["users", 1], { id: 1, name: "John" });
render(
<QueryClientProvider client={queryClient}>
<UserProfile userId={1} />
</QueryClientProvider>
);
expect(screen.getByText("John")).toBeInTheDocument();
});
Общие паттерны избежания Transport Coupling
1. Repository Pattern
interface UserRepository {
getUser(id: number): Promise<User>;
createUser(user: Omit<User, "id">): Promise<User>;
}
// HTTP реализация
class HttpUserRepository implements UserRepository {
constructor(private httpClient: HttpClient) {}
async getUser(id: number) {
return this.httpClient.get(`/api/v1/users/${id}`);
}
}
// GraphQL реализация
class GraphQLUserRepository implements UserRepository {
constructor(private apolloClient: ApolloClient) {}
async getUser(id: number) {
const result = await this.apolloClient.query({
query: GET_USER,
variables: { id }
});
return result.data.user;
}
}
// Компонент работает с обеими имплементациями
class UserService {
constructor(private repository: UserRepository) {}
async getUser(id: number) {
return this.repository.getUser(id);
}
}
2. Adapter Pattern
// Адаптер для разных API клиентов
class ApiAdapter {
constructor(private client: AxiosInstance | fetch) {}
async request<T>(config: RequestConfig): Promise<T> {
if (typeof this.client === "function") {
// fetch
const response = await this.client(config.url, config.options);
return response.json();
} else {
// axios
return this.client.request(config).then(r => r.data);
}
}
}
3. Мокирование на уровне абстракции
// Вместо мокирования fetch - мокируем наш сервис
class MockUserService {
async getUser(id) {
return { id, name: "Mock User" };
}
}
// Используем в компоненте
function UserProfile({ userService }) {
const [user, setUser] = React.useState(null);
React.useEffect(() => {
userService.getUser(1).then(setUser);
}, [userService]);
return <div>{user?.name}</div>;
}
// Тест
test("displays user", async () => {
const mockService = new MockUserService();
render(<UserProfile userService={mockService} />);
// Не нужно мокировать fetch!
});
Заключение
Transport Coupling - это когда тесты зависят от конкретного способа передачи данных. Как избежать:
- Используйте Dependency Injection - внедряйте зависимости, не создавайте их внутри
- Создавайте абстракции - HttpClient, Repository, и т.д.
- Мокируйте на уровне абстракции - не мокируйте fetch/axios
- Используйте Query Client - React Query, SWR обеспечивают абстракцию
- Следуйте SOLID - особенно Dependency Inversion Principle
Это делает тесты более стабильными, читаемыми и легче менять реализацию.