← Назад к вопросам

Есть ли привязка к транспорту в тестах?

2.0 Middle🔥 191 комментариев
#Тестирование

Комментарии (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 - это когда тесты зависят от конкретного способа передачи данных. Как избежать:

  1. Используйте Dependency Injection - внедряйте зависимости, не создавайте их внутри
  2. Создавайте абстракции - HttpClient, Repository, и т.д.
  3. Мокируйте на уровне абстракции - не мокируйте fetch/axios
  4. Используйте Query Client - React Query, SWR обеспечивают абстракцию
  5. Следуйте SOLID - особенно Dependency Inversion Principle

Это делает тесты более стабильными, читаемыми и легче менять реализацию.