MiddleКодРедкоЕщё не отвечали
Спроектируйте тестируемый сервис продуктов поверх БД
Спроектируйте сервис, который получает продукты из базы данных по фильтру и выводит их, и который юнит-тестируется без реальной БД.
Требования:
- зависьте от абстрактного
IProductRepository, а не от конкретного класса БД (Dependency Injection) - предоставьте
FakeProductRepository, чтобы тесты шли в памяти, без подключения к БД - интерфейс требует
virtualдеструктор; возвращайте результаты по значению, не сырыми указателями
struct Product { int id; std::string name; double price; std::string category; };
struct ProductFilter { std::optional<std::string> category; std::optional<double> maxPrice; };
class IProductRepository {
public:
// ваш код здесь
};
class FakeProductRepository : public IProductRepository {
// ваш код здесь
};
class ProductService {
public:
explicit ProductService(IProductRepository& repo);
void printByFilter(const ProductFilter& f) const;
// ваш код здесь
};
Допишите реализацию.
Определите IProductRepository (чистый виртуальный fetchByFilter), SqlProductRepository для реальных запросов, FakeProductRepository для тестов. ProductService зависит только от интерфейса — Dependency Injection позволяет тесты без БД.
- ✗Хардкодить вызов базы данных внутри сервиса — невозможно юнит-тестировать без реальной БД
- ✗Не делать деструктор интерфейса репозитория виртуальным — удаление через указатель на базу является UB
- ✗Возвращать сырые указатели из репозитория — используйте
std::vector<Product>по значению или умные указатели
- →Как добавить пагинацию в
fetchByFilter? - →Как изменится этот дизайн, если репозиторий должен быть потокобезопасным?
Оглавление
Задача
Реализуйте:
- Структуру
Product(id, name, price, category) - Структуру
ProductFilter(опциональная категория, максимальная цена) - Интерфейс
IProductRepositoryс методомfetchByFilter - Заглушку
FakeProductRepositoryдля тестов - Сервис
ProductService, который использует репозиторий и выводит продукты
Решение
#include <iostream>
#include <string>
#include <vector>
#include <optional>
#include <memory>
#include <algorithm>
#include <cassert>
// --- Domain types ---
struct Product {
int id;
std::string name;
double price;
std::string category;
};
struct ProductFilter {
std::optional<std::string> category;
std::optional<double> maxPrice;
};
// --- Repository interface ---
class IProductRepository {
public:
virtual ~IProductRepository() = default;
virtual std::vector<Product> fetchByFilter(const ProductFilter& f) const = 0;
};
// --- Fake for tests ---
class FakeProductRepository : public IProductRepository {
public:
void add(Product p) { products_.push_back(std::move(p)); }
std::vector<Product> fetchByFilter(const ProductFilter& f) const override {
std::vector<Product> result;
for (const auto& p : products_) {
if (f.category && p.category != *f.category) continue;
if (f.maxPrice && p.price > *f.maxPrice) continue;
result.push_back(p);
}
return result;
}
private:
std::vector<Product> products_;
};
// --- Real repository (stub — actual SQL omitted) ---
class SqlProductRepository : public IProductRepository {
public:
explicit SqlProductRepository(const std::string& /*connStr*/) {}
std::vector<Product> fetchByFilter(const ProductFilter& /*f*/) const override {
// In production: build SQL WHERE clause, execute query, map rows to Product.
return {};
}
};
// --- Service ---
class ProductService {
public:
explicit ProductService(IProductRepository& repo) : repo_(repo) {}
void printByFilter(const ProductFilter& f) const {
auto products = repo_.fetchByFilter(f);
if (products.empty()) {
std::cout << "(no products match the filter)\n";
return;
}
for (const auto& p : products)
std::cout << "[" << p.id << "] " << p.name
<< " | " << p.category
<< " | $" << p.price << '\n';
}
private:
IProductRepository& repo_;
};
// --- Tests ---
static void testFilterByCategory() {
FakeProductRepository repo;
repo.add({1, "Widget", 9.99, "tools"});
repo.add({2, "Gadget", 19.99, "electronics"});
repo.add({3, "Doohickey", 4.99, "tools"});
auto results = repo.fetchByFilter({.category = "tools"});
assert(results.size() == 2);
assert(results[0].name == "Widget");
assert(results[1].name == "Doohickey");
}
static void testFilterByMaxPrice() {
FakeProductRepository repo;
repo.add({1, "Cheap", 5.0, "misc"});
repo.add({2, "Mid", 15.0, "misc"});
repo.add({3, "Pricey", 50.0, "misc"});
auto results = repo.fetchByFilter({.maxPrice = 20.0});
assert(results.size() == 2);
}
static void testNoResults() {
FakeProductRepository repo;
repo.add({1, "Widget", 9.99, "tools"});
auto results = repo.fetchByFilter({.category = "food"});
assert(results.empty());
}
int main() {
testFilterByCategory();
testFilterByMaxPrice();
testNoResults();
std::cout << "All tests passed\n\n";
// Service demo
FakeProductRepository repo;
repo.add({1, "Hammer", 12.50, "tools"});
repo.add({2, "Keyboard", 89.00, "electronics"});
repo.add({3, "Screwdriver", 7.99, "tools"});
ProductService svc(repo);
std::cout << "--- tools under $15 ---\n";
svc.printByFilter({.category = "tools", .maxPrice = 15.0});
return 0;
}
Ключевые моменты
| Аспект | Решение |
|---|---|
| Dependency Injection | ProductService принимает IProductRepository& → легко подставить заглушку |
| Тестируемость | FakeProductRepository реализует интерфейс в памяти |
| Виртуальный деструктор | IProductRepository::~IProductRepository() = default |
| Возврат данных | std::vector<Product> по значению — безопасно, понятно |
Оглавление