| name | test-generation |
| description | Use when creating or enhancing unit tests, integration tests, or defining test strategies for backend and frontend code. |
| allowed-tools | Read, Write, Edit, Grep, Glob, Bash |
Test Generation Workflow
When to Use This Skill
- Creating unit tests for new code
- Adding tests for bug fixes
- Integration test development
- Test coverage improvement
Pre-Flight Checklist
- Identify code to test (command, query, entity, component)
- Find existing test patterns:
grep "Test.*{Feature}" --include="*.cs" - Determine test type (unit, integration, e2e)
- Identify dependencies to mock
File Locations
Backend Tests
tests/
└── {Service}.Tests/
├── UnitTests/
│ ├── Commands/
│ │ └── Save{Entity}CommandTests.cs
│ ├── Queries/
│ │ └── Get{Entity}ListQueryTests.cs
│ └── Entities/
│ └── {Entity}Tests.cs
└── IntegrationTests/
└── {Feature}IntegrationTests.cs
Frontend Tests
src/PlatformExampleAppWeb/apps/{app}/src/app/
└── features/
└── {feature}/
├── {feature}.component.spec.ts
└── {feature}.store.spec.ts
Pattern 1: Command Handler Unit Test
public class SaveEmployeeCommandTests
{
private readonly Mock<IPlatformQueryableRootRepository<Employee, string>> _employeeRepoMock;
private readonly Mock<IPlatformApplicationRequestContextAccessor> _contextMock;
private readonly SaveEmployeeCommandHandler _handler;
public SaveEmployeeCommandTests()
{
_employeeRepoMock = new Mock<IPlatformQueryableRootRepository<Employee, string>>();
_contextMock = new Mock<IPlatformApplicationRequestContextAccessor>();
// Setup default context
var requestContext = new Mock<IPlatformApplicationRequestContext>();
requestContext.Setup(x => x.UserId()).Returns("test-user-id");
requestContext.Setup(x => x.CurrentCompanyId()).Returns("test-company-id");
_contextMock.Setup(x => x.Current).Returns(requestContext.Object);
_handler = new SaveEmployeeCommandHandler(
Mock.Of<ILoggerFactory>(),
Mock.Of<IPlatformUnitOfWorkManager>(),
Mock.Of<IServiceProvider>(),
Mock.Of<IPlatformRootServiceProvider>(),
_employeeRepoMock.Object
);
}
[Fact]
public async Task HandleAsync_CreateEmployee_ReturnsNewEmployee()
{
// Arrange
var command = new SaveEmployeeCommand
{
Name = "John Doe",
Email = "john@example.com"
};
_employeeRepoMock
.Setup(x => x.CreateAsync(It.IsAny<Employee>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((Employee e, CancellationToken _) => e);
// Act
var result = await _handler.HandleAsync(command, CancellationToken.None);
// Assert
Assert.NotNull(result.Employee);
Assert.Equal("John Doe", result.Employee.Name);
_employeeRepoMock.Verify(x => x.CreateAsync(
It.Is<Employee>(e => e.Name == "John Doe"),
It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task HandleAsync_UpdateEmployee_UpdatesExisting()
{
// Arrange
var existingEmployee = new Employee { Id = "emp-1", Name = "Old Name" };
var command = new SaveEmployeeCommand
{
Id = "emp-1",
Name = "New Name"
};
_employeeRepoMock
.Setup(x => x.GetByIdAsync("emp-1", It.IsAny<CancellationToken>()))
.ReturnsAsync(existingEmployee);
_employeeRepoMock
.Setup(x => x.UpdateAsync(It.IsAny<Employee>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((Employee e, CancellationToken _) => e);
// Act
var result = await _handler.HandleAsync(command, CancellationToken.None);
// Assert
Assert.Equal("New Name", result.Employee.Name);
_employeeRepoMock.Verify(x => x.UpdateAsync(
It.Is<Employee>(e => e.Name == "New Name"),
It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task HandleAsync_InvalidCommand_ReturnsValidationError()
{
// Arrange
var command = new SaveEmployeeCommand
{
Name = "" // Invalid: empty name
};
// Act & Assert
var result = command.Validate();
Assert.False(result.IsValid);
Assert.Contains(result.Errors, e => e.Contains("Name"));
}
}
Pattern 2: Query Handler Unit Test
public class GetEmployeeListQueryTests
{
private readonly Mock<IPlatformQueryableRootRepository<Employee, string>> _repoMock;
private readonly GetEmployeeListQueryHandler _handler;
[Fact]
public async Task HandleAsync_WithFilters_ReturnsFilteredResults()
{
// Arrange
var employees = new List<Employee>
{
new() { Id = "1", Name = "Active", Status = EmployeeStatus.Active },
new() { Id = "2", Name = "Inactive", Status = EmployeeStatus.Inactive }
};
_repoMock.Setup(x => x.CountAsync(It.IsAny<Expression<Func<Employee, bool>>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(1);
_repoMock.Setup(x => x.GetAllAsync(It.IsAny<Func<IPlatformUnitOfWork, IQueryable<Employee>, IQueryable<Employee>>>(), It.IsAny<CancellationToken>(), It.IsAny<Expression<Func<Employee, object>>[]>()))
.ReturnsAsync(employees.Where(e => e.Status == EmployeeStatus.Active).ToList());
var query = new GetEmployeeListQuery
{
Statuses = [EmployeeStatus.Active],
SkipCount = 0,
MaxResultCount = 10
};
// Act
var result = await _handler.HandleAsync(query, CancellationToken.None);
// Assert
Assert.Single(result.Items);
Assert.Equal("Active", result.Items[0].Name);
}
}
Pattern 3: Entity Validation Test
public class EmployeeEntityTests
{
[Fact]
public void UniqueExpr_ReturnsCorrectExpression()
{
// Arrange
var employees = new List<Employee>
{
new() { CompanyId = "c1", UserId = "u1" },
new() { CompanyId = "c1", UserId = "u2" },
new() { CompanyId = "c2", UserId = "u1" }
}.AsQueryable();
// Act
var expr = Employee.UniqueExpr("c1", "u1");
var result = employees.Where(expr).ToList();
// Assert
Assert.Single(result);
Assert.Equal("u1", result[0].UserId);
}
[Fact]
public async Task ValidateAsync_DuplicateCode_ReturnsError()
{
// Arrange
var repoMock = new Mock<IPlatformQueryableRootRepository<Employee, string>>();
repoMock.Setup(x => x.AnyAsync(It.IsAny<Expression<Func<Employee, bool>>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(true); // Duplicate exists
var employee = new Employee { Id = "new", Code = "EMP001", CompanyId = "c1" };
// Act
var result = await employee.ValidateAsync(repoMock.Object, CancellationToken.None);
// Assert
Assert.False(result.IsValid);
Assert.Contains(result.Errors, e => e.Contains("already exists"));
}
[Fact]
public void ComputedProperty_IsActive_CalculatesCorrectly()
{
// Arrange
var activeEmployee = new Employee { Status = EmployeeStatus.Active, IsDeleted = false };
var inactiveEmployee = new Employee { Status = EmployeeStatus.Inactive, IsDeleted = false };
var deletedEmployee = new Employee { Status = EmployeeStatus.Active, IsDeleted = true };
// Assert
Assert.True(activeEmployee.IsActive);
Assert.False(inactiveEmployee.IsActive);
Assert.False(deletedEmployee.IsActive);
}
}
Pattern 4: Angular Component Test
describe('FeatureListComponent', () => {
let component: FeatureListComponent;
let fixture: ComponentFixture<FeatureListComponent>;
let store: FeatureListStore;
let apiMock: jasmine.SpyObj<FeatureApiService>;
beforeEach(async () => {
apiMock = jasmine.createSpyObj('FeatureApiService', ['getList', 'delete']);
await TestBed.configureTestingModule({
imports: [FeatureListComponent],
providers: [
FeatureListStore,
{ provide: FeatureApiService, useValue: apiMock }
]
}).compileComponents();
fixture = TestBed.createComponent(FeatureListComponent);
component = fixture.componentInstance;
store = TestBed.inject(FeatureListStore);
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should load items on init', () => {
// Arrange
const items = [{ id: '1', name: 'Test' }];
apiMock.getList.and.returnValue(of({ items, totalCount: 1 }));
// Act
fixture.detectChanges();
// Assert
expect(apiMock.getList).toHaveBeenCalled();
expect(component.vm()?.items).toEqual(items);
});
it('should delete item', fakeAsync(() => {
// Arrange
store.updateState({ items: [{ id: '1', name: 'Test' }] });
apiMock.delete.and.returnValue(of(void 0));
// Act
component.onDelete({ id: '1', name: 'Test' });
tick();
// Assert
expect(apiMock.delete).toHaveBeenCalledWith('1');
expect(component.vm()?.items.length).toBe(0);
}));
it('should show loading state', () => {
// Arrange
apiMock.getList.and.returnValue(new Subject()); // Never completes
// Act
fixture.detectChanges();
// Assert
expect(store.isLoading$('loadItems')()).toBe(true);
});
});
Pattern 5: Angular Store Test
describe('FeatureListStore', () => {
let store: FeatureListStore;
let apiMock: jasmine.SpyObj<FeatureApiService>;
beforeEach(() => {
apiMock = jasmine.createSpyObj('FeatureApiService', ['getList', 'save', 'delete']);
TestBed.configureTestingModule({
providers: [
FeatureListStore,
{ provide: FeatureApiService, useValue: apiMock }
]
});
store = TestBed.inject(FeatureListStore);
});
it('should initialize with default state', () => {
expect(store.currentVm().items).toEqual([]);
expect(store.currentVm().pagination.pageIndex).toBe(0);
});
it('should load items', fakeAsync(() => {
// Arrange
const items = [{ id: '1', name: 'Test' }];
apiMock.getList.and.returnValue(of({ items, totalCount: 1 }));
// Act
store.loadItems();
tick();
// Assert
expect(store.currentVm().items).toEqual(items);
expect(store.currentVm().pagination.totalCount).toBe(1);
}));
it('should update state immutably', () => {
// Arrange
const initialItems = store.currentVm().items;
// Act
store.updateState({ items: [{ id: '1', name: 'New' }] });
// Assert
expect(store.currentVm().items).not.toBe(initialItems);
});
it('should handle API error', fakeAsync(() => {
// Arrange
apiMock.getList.and.returnValue(throwError(() => new Error('API Error')));
// Act
store.loadItems();
tick();
// Assert
expect(store.getErrorMsg$('loadItems')()).toContain('Error');
}));
});
Test Naming Convention
[MethodName]_[Scenario]_[ExpectedBehavior]
Examples:
- HandleAsync_ValidCommand_ReturnsSuccess
- HandleAsync_InvalidId_ThrowsNotFound
- UniqueExpr_MatchingValues_ReturnsTrue
- LoadItems_ApiError_SetsErrorState
Anti-Patterns to AVOID
:x: Testing implementation, not behavior
// WRONG - testing internal method calls
Assert.True(handler.WasValidateCalled);
// CORRECT - testing observable behavior
Assert.Equal("Expected", result.Value);
:x: Not mocking dependencies
// WRONG - using real repository
var handler = new Handler(new RealRepository());
// CORRECT - using mock
var repoMock = new Mock<IRepository>();
var handler = new Handler(repoMock.Object);
:x: Missing edge cases
// WRONG - only happy path
[Fact] public void Save_ValidData_Succeeds() { }
// CORRECT - include edge cases
[Fact] public void Save_EmptyName_ReturnsError() { }
[Fact] public void Save_DuplicateCode_ReturnsError() { }
[Fact] public void Save_NullInput_ThrowsException() { }
Verification Checklist
- Unit tests cover happy path
- Edge cases and error conditions tested
- Dependencies properly mocked
- Test naming follows convention
- Assertions are specific and meaningful
- No test interdependencies
- Tests are deterministic (no random, no time-dependent)