47+
Existing Tests
5
Test Projects
40-50%
Current Coverage
90%
Target Coverage

Testing Strategy Overview

The Shirinzad E-Commerce Platform employs a comprehensive, multi-layered testing strategy to ensure code quality, reliability, and maintainability across all modules.

Testing Pyramid

graph TD A[E2E Tests
5% - UI & Integration] --> B[Integration Tests
20% - API & Database] B --> C[Unit Tests
75% - Business Logic] style A fill:#dc2626 style B fill:#d97706 style C fill:#059669

Key Principles

  • Unit tests for business logic and domain entities
  • Integration tests for application services with in-memory database
  • Repository tests for data access layer
  • API endpoint tests for HTTP layer
  • Automated CI/CD test execution
  • Test-driven development (TDD) for critical features

Test Frameworks & Tools

xUnit

Purpose: Test runner and framework

Version: 2.4.2+

  • Industry-standard .NET test framework
  • Parallel test execution
  • Extensible architecture
  • Excellent Visual Studio integration

Shouldly

Purpose: Assertion library

Version: 4.0+

  • Readable assertion syntax
  • Better error messages
  • Fluent API
  • Natural language assertions

NSubstitute

Purpose: Mocking framework

Version: 5.0+

  • Simple, elegant mocking syntax
  • Interface and class mocking
  • Argument matching
  • Return value configuration

Test Projects Structure

Project Organization

graph TD A[test/] --> B[Shirinzad.Shop.TestBase] A --> C[Shirinzad.Shop.Domain.Tests] A --> D[Shirinzad.Shop.Application.Tests] A --> E[Shirinzad.Shop.EntityFrameworkCore.Tests] A --> F[Shirinzad.Shop.Web.Tests] A --> G[Shirinzad.Shop.HttpApi.Client.ConsoleTestApp] B --> C B --> D B --> E B --> F style A fill:#6366f1 style B fill:#059669

1. Shirinzad.Shop.TestBase

Purpose: Shared test infrastructure and utilities

Key Components:

Component Description
ShopTestBase<TModule> Base class for all tests with ABP integration
ShopTestDataSeedContributor Seed test data for integration tests
ShopTestConsts Test constants and configuration
FakeCurrentPrincipalAccessor Mock authentication for security tests

2. Shirinzad.Shop.Domain.Tests

Purpose: Unit tests for domain entities and business logic

Test Count: 8+ tests

Test Coverage:

  • Product entity validation and business rules
  • Category hierarchy and relationships
  • Order state transitions
  • Domain events and event handlers
  • Value objects validation

Example Tests:

// test/Shirinzad.Shop.Domain.Tests/Catalog/ProductTests.cs
[Fact]
public void Should_Create_Product_With_Valid_Data()
{
    // Arrange & Act
    var product = new Product(
        Guid.NewGuid(),
        "Test Product",
        "test-product"
    );

    // Assert
    product.Name.ShouldBe("Test Product");
    product.Slug.ShouldBe("test-product");
    product.IsActive.ShouldBeTrue();
}

[Fact]
public void Should_Calculate_Discount_Percentage_Correctly()
{
    // Arrange
    var product = new Product(Guid.NewGuid(), "Product", "product");
    product.UpdateBasicInfo(
        name: "Product",
        slug: "product",
        price: 100000m,
        discountPrice: 75000m
    );

    // Act
    var discountPercentage = product.GetDiscountPercentage();

    // Assert
    discountPercentage.ShouldBe(25);
}

3. Shirinzad.Shop.Application.Tests

Purpose: Integration tests for application services

Test Count: 35+ tests

Test Coverage by Module:

Module Test File Tests
Products ProductAppService_Tests.cs 14 tests
Categories CategoryAppService_Tests.cs 6 tests
Brands BrandAppService_Tests.cs 4 tests
Tags TagAppService_Tests.cs 3 tests
Orders OrderAppService_Tests.cs 5 tests
Payments PaymentAppService_Tests.cs 3 tests
Dashboard DashboardAppService_Tests.cs 2 tests

Comprehensive Product Test Example:

public class ProductAppService_Tests : ShopApplicationTestBase<ShopApplicationTestModule>
{
    private readonly IProductAppService _productAppService;
    private readonly IRepository<Product, Guid> _productRepository;

    public ProductAppService_Tests()
    {
        _productAppService = GetRequiredService<IProductAppService>();
        _productRepository = GetRequiredService<IRepository<Product, Guid>>();
    }

    [Fact]
    public async Task Should_Create_Product_With_Valid_Data()
    {
        // Arrange
        var brand = await CreateBrandAsync("Test Brand");
        var category = await CreateCategoryAsync("Test Category");

        var createDto = new CreateProductDto
        {
            Name = "Test Product",
            Slug = "test-product",
            Price = 100000m,
            DiscountPrice = 80000m,
            Stock = 50,
            BrandId = brand.Id,
            CategoryIds = new List<Guid> { category.Id },
            IsFeatured = true
        };

        // Act
        var result = await _productAppService.CreateAsync(createDto);

        // Assert
        result.ShouldNotBeNull();
        result.Id.ShouldNotBe(Guid.Empty);
        result.Name.ShouldBe("Test Product");
        result.Price.ShouldBe(100000m);
        result.Stock.ShouldBe(50);

        // Verify in database
        var productInDb = await _productRepository.GetAsync(result.Id);
        productInDb.Name.ShouldBe("Test Product");
    }

    [Fact]
    public async Task Should_Update_Product()
    {
        // Arrange
        var product = await CreateProductAsync("Original Product");
        var updateDto = new UpdateProductDto
        {
            Name = "Updated Product",
            Price = 200000m
        };

        // Act
        var result = await _productAppService.UpdateAsync(product.Id, updateDto);

        // Assert
        result.Name.ShouldBe("Updated Product");
        result.Price.ShouldBe(200000m);
    }

    [Fact]
    public async Task Should_Get_Product_List_With_Pagination()
    {
        // Arrange - Create 15 products
        for (int i = 1; i <= 15; i++)
        {
            await CreateProductAsync($"Product {i}");
        }

        // Act
        var result = await _productAppService.GetListAsync(
            new PagedAndSortedResultRequestDto
            {
                SkipCount = 0,
                MaxResultCount = 10
            }
        );

        // Assert
        result.TotalCount.ShouldBe(15);
        result.Items.Count.ShouldBe(10);
    }
}

4. Shirinzad.Shop.EntityFrameworkCore.Tests

Purpose: Repository and database tests with in-memory database

Test Count: 3+ tests

Features:

  • In-memory SQLite database for fast testing
  • Repository pattern validation
  • Query performance testing
  • Database migration validation

Example Repository Test:

public class SampleRepositoryTests : ShopEntityFrameworkCoreTestBase
{
    private readonly IRepository<Product, Guid> _productRepository;

    public SampleRepositoryTests()
    {
        _productRepository = GetRequiredService<IRepository<Product, Guid>>();
    }

    [Fact]
    public async Task Should_Query_Products_Efficiently()
    {
        // Arrange
        await WithUnitOfWorkAsync(async () =>
        {
            await _productRepository.InsertAsync(
                new Product(Guid.NewGuid(), "Product 1", "product-1")
            );
        });

        // Act
        var products = await WithUnitOfWorkAsync(async () =>
        {
            return await _productRepository.GetListAsync();
        });

        // Assert
        products.Count.ShouldBeGreaterThan(0);
    }
}

5. Shirinzad.Shop.Web.Tests

Purpose: Web layer and controller tests

Test Count: 2+ tests

Features:

  • API endpoint integration tests
  • HTTP request/response validation
  • Authorization testing
  • Controller action tests

Example Controller Test:

public class ConfigController_Tests : ShopWebTestBase
{
    [Fact]
    public async Task Should_Get_Config_Settings()
    {
        // Act
        var result = await GetResponseAsObjectAsync<ConfigDto>(
            "/api/config/shirinzad"
        );

        // Assert
        result.ShouldNotBeNull();
        result.SiteName.ShouldNotBeNullOrEmpty();
    }
}

Test Coverage Analysis

Current Coverage: 40-50%

Module Existing Tests Coverage Status
Catalog (Products, Categories, Brands, Tags) 27 tests ~70% Good
Orders & Payments 8 tests ~50% Medium
Configuration 2 tests ~60% Medium
Dashboard 2 tests ~40% Medium
Carts 0 tests 0% Missing
Discounts 0 tests 0% Missing
Shipments 0 tests 0% Missing
Reviews 0 tests 0% Missing
Wishlist 0 tests 0% Missing
Notifications 0 tests 0% Missing
Blog & CMS 0 tests 0% Missing
Automotive 0 tests 0% Missing
Gap Analysis: To achieve 90% coverage target, approximately 150-200 additional tests are required across 12 untested modules.

Testing Gaps & Recommendations

Priority 1: Critical Gaps

  • Cart Management: 15-20 tests needed
    • Add/remove items
    • Update quantities
    • Price calculations
    • Cart expiration
  • Discount Engine: 12-15 tests needed
    • Percentage discounts
    • Fixed amount discounts
    • Coupon codes
    • Discount stacking rules
  • Payment Processing: 10-12 tests needed
    • Payment gateway integration
    • Transaction validation
    • Refund processing
    • Failed payment handling

Priority 2: Important Gaps

  • Shipment Tracking: 8-10 tests needed
    • Shipment creation
    • Status updates
    • Carrier integration
    • Delivery confirmation
  • Review System: 10-12 tests needed
    • Review submission
    • Rating calculations
    • Review moderation
    • Helpful votes
  • Wishlist: 8-10 tests needed
    • Add/remove items
    • Multiple wishlists
    • Privacy settings
    • Move to cart

Priority 3: Enhancement Gaps

  • Notifications: 12-15 tests needed
    • Email notifications
    • SMS notifications
    • In-app notifications
    • Notification preferences
  • Blog & CMS: 15-18 tests needed
    • Post CRUD operations
    • Category management
    • Page management
    • SEO metadata
  • Automotive Module: 20-25 tests needed
    • Vehicle management
    • Service scheduling
    • Compatibility checking
    • VIN decoding

Priority 4: Integration Tests

  • End-to-End Scenarios: 15-20 tests
    • Complete checkout flow
    • Order fulfillment cycle
    • User registration to purchase
    • Returns and refunds
  • Performance Tests: 10-12 tests
    • Load testing critical endpoints
    • Database query optimization
    • Cache effectiveness
    • Concurrent user scenarios
  • Security Tests: 8-10 tests
    • Authentication flows
    • Authorization policies
    • Rate limiting
    • Input validation

Running Tests

Run All Tests

# Run all tests in solution
dotnet test

# Run with detailed output
dotnet test --verbosity detailed

# Run with code coverage
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura

Run Specific Test Project

# Run domain tests only
dotnet test test/Shirinzad.Shop.Domain.Tests

# Run application tests only
dotnet test test/Shirinzad.Shop.Application.Tests

# Run specific test class
dotnet test --filter "FullyQualifiedName~ProductAppService_Tests"

# Run specific test method
dotnet test --filter "FullyQualifiedName~Should_Create_Product_With_Valid_Data"

Visual Studio Test Explorer

  1. Open Test Explorer: Test > Test Explorer
  2. Build solution to discover tests
  3. Click "Run All" or select specific tests
  4. View results in Test Explorer window
  5. Debug failing tests by right-clicking and selecting "Debug"

CI/CD Integration

# GitHub Actions example
name: Run Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup .NET
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: '9.0.x'
      - name: Restore dependencies
        run: dotnet restore
      - name: Build
        run: dotnet build --no-restore
      - name: Test
        run: dotnet test --no-build --verbosity normal --collect:"XPlat Code Coverage"
      - name: Upload coverage reports
        uses: codecov/codecov-action@v3

Test Data Seeding

ShopTestDataSeedContributor

The test base project provides a data seeding mechanism for integration tests:

public class ShopTestDataSeedContributor : IDataSeedContributor, ITransientDependency
{
    public Task SeedAsync(DataSeedContext context)
    {
        // Seed additional test data
        // This runs before each test execution

        return Task.CompletedTask;
    }
}

Custom Test Data Creation

// Helper methods in test classes
private async Task<Product> CreateProductAsync(
    string name,
    string slug,
    decimal price = 100000m,
    int stock = 10)
{
    var product = new Product(Guid.NewGuid(), name, slug);
    product.UpdateBasicInfo(
        name: name,
        slug: slug,
        price: price,
        stockQuantity: stock
    );

    return await _productRepository.InsertAsync(product, autoSave: true);
}

private async Task<Category> CreateCategoryAsync(string name)
{
    var category = new Category(
        Guid.NewGuid(),
        name,
        name.ToLower().Replace(" ", "-")
    );

    return await _categoryRepository.InsertAsync(category, autoSave: true);
}

Testing Best Practices

Unit Testing Guidelines

  • Follow AAA pattern: Arrange, Act, Assert
  • One assertion per test (when possible)
  • Use descriptive test names (Should_Do_Something_When_Condition)
  • Keep tests isolated and independent
  • Mock external dependencies
  • Test edge cases and error conditions
  • Avoid test interdependencies

Integration Testing Guidelines

  • Use in-memory database for speed
  • Clean up data after each test
  • Test realistic scenarios
  • Verify database state changes
  • Test transaction rollback
  • Include pagination and filtering tests
  • Test concurrent operations

Test Naming Conventions

// Good test names
Should_Create_Product_With_Valid_Data()
Should_Throw_Exception_When_Price_Is_Negative()
Should_Return_Empty_List_When_No_Products_Exist()
Should_Update_Stock_Quantity_After_Order()

// Bad test names
Test1()
ProductTest()
CreateProduct()
TestProductCreation()

Assertion Best Practices

// Using Shouldly for readable assertions

// Good
result.ShouldNotBeNull();
result.Name.ShouldBe("Expected Name");
result.Price.ShouldBe(100000m);
result.Items.Count.ShouldBe(5);

// Also good - testing multiple properties
result.ShouldSatisfyAllConditions(
    () => result.Name.ShouldBe("Expected"),
    () => result.Price.ShouldBeGreaterThan(0),
    () => result.IsActive.ShouldBeTrue()
);

Mocking with NSubstitute

Basic Mocking

// Mock repository
var mockRepository = Substitute.For<IRepository<Product, Guid>>();

// Configure return value
mockRepository.GetAsync(Arg.Any<Guid>())
    .Returns(new Product(Guid.NewGuid(), "Test Product", "test-product"));

// Verify method was called
await mockRepository.Received(1).GetAsync(Arg.Any<Guid>());

Advanced Mocking Scenarios

// Mock with specific argument matching
mockRepository.GetListAsync(
    Arg.Is<Expression<Func<Product, bool>>>(x => x != null)
).Returns(new List<Product> { product1, product2 });

// Mock to throw exception
mockRepository.InsertAsync(Arg.Any<Product>())
    .Throws<InvalidOperationException>();

// Mock with callback
mockRepository.When(x => x.DeleteAsync(Arg.Any<Guid>()))
    .Do(x => Console.WriteLine("Delete called"));

Testing Roadmap

Phase 1: Foundation (Weeks 1-2)

  • Complete Cart module tests (15-20 tests)
  • Complete Discount module tests (12-15 tests)
  • Add Payment integration tests (10-12 tests)

Phase 2: Core Features (Weeks 3-4)

  • Complete Shipment module tests (8-10 tests)
  • Complete Review system tests (10-12 tests)
  • Complete Wishlist module tests (8-10 tests)

Phase 3: Advanced Features (Weeks 5-6)

  • Complete Notification tests (12-15 tests)
  • Complete Blog & CMS tests (15-18 tests)
  • Complete Automotive module tests (20-25 tests)

Phase 4: Quality Assurance (Weeks 7-8)

  • Add end-to-end scenario tests (15-20 tests)
  • Add performance tests (10-12 tests)
  • Add security tests (8-10 tests)
  • Achieve 90%+ code coverage

Additional Resources