-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathtesting-patterns.mdc
More file actions
274 lines (225 loc) · 6.49 KB
/
testing-patterns.mdc
File metadata and controls
274 lines (225 loc) · 6.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
---
description: "ABP testing patterns - unit tests and integration tests"
globs:
- "test/**/*.cs"
- "tests/**/*.cs"
- "**/*Tests*/**/*.cs"
- "**/*Test*.cs"
alwaysApply: false
---
# ABP Testing Patterns
> **Docs**: https://abp.io/docs/latest/testing
## Test Project Structure
| Project | Purpose | Base Class |
|---------|---------|------------|
| `*.Domain.Tests` | Domain logic, entities, domain services | `*DomainTestBase` |
| `*.Application.Tests` | Application services | `*ApplicationTestBase` |
| `*.EntityFrameworkCore.Tests` | Repository implementations | `*EntityFrameworkCoreTestBase` |
## Integration Test Approach
ABP recommends integration tests over unit tests:
- Tests run with real services and database (SQLite in-memory)
- No mocking of internal services
- Each test gets a fresh database instance
## Application Service Test
```csharp
public class BookAppService_Tests : MyProjectApplicationTestBase
{
private readonly IBookAppService _bookAppService;
public BookAppService_Tests()
{
_bookAppService = GetRequiredService<IBookAppService>();
}
[Fact]
public async Task Should_Get_List_Of_Books()
{
// Act
var result = await _bookAppService.GetListAsync(
new PagedAndSortedResultRequestDto()
);
// Assert
result.TotalCount.ShouldBeGreaterThan(0);
result.Items.ShouldContain(b => b.Name == "Test Book");
}
[Fact]
public async Task Should_Create_Book()
{
// Arrange
var input = new CreateBookDto
{
Name = "New Book",
Price = 19.99m
};
// Act
var result = await _bookAppService.CreateAsync(input);
// Assert
result.Id.ShouldNotBe(Guid.Empty);
result.Name.ShouldBe("New Book");
result.Price.ShouldBe(19.99m);
}
[Fact]
public async Task Should_Not_Create_Book_With_Invalid_Name()
{
// Arrange
var input = new CreateBookDto
{
Name = "", // Invalid
Price = 10m
};
// Act & Assert
await Should.ThrowAsync<AbpValidationException>(async () =>
{
await _bookAppService.CreateAsync(input);
});
}
}
```
## Domain Service Test
```csharp
public class BookManager_Tests : MyProjectDomainTestBase
{
private readonly BookManager _bookManager;
private readonly IBookRepository _bookRepository;
public BookManager_Tests()
{
_bookManager = GetRequiredService<BookManager>();
_bookRepository = GetRequiredService<IBookRepository>();
}
[Fact]
public async Task Should_Create_Book()
{
// Act
var book = await _bookManager.CreateAsync("Test Book", 29.99m);
// Assert
book.ShouldNotBeNull();
book.Name.ShouldBe("Test Book");
book.Price.ShouldBe(29.99m);
}
[Fact]
public async Task Should_Not_Allow_Duplicate_Book_Name()
{
// Arrange
await _bookManager.CreateAsync("Existing Book", 10m);
// Act & Assert
var exception = await Should.ThrowAsync<BusinessException>(async () =>
{
await _bookManager.CreateAsync("Existing Book", 20m);
});
exception.Code.ShouldBe("MyProject:BookNameAlreadyExists");
}
}
```
## Test Naming Convention
Use descriptive names:
```csharp
// Pattern: Should_ExpectedBehavior_When_Condition
public async Task Should_Create_Book_When_Input_Is_Valid()
public async Task Should_Throw_BusinessException_When_Name_Already_Exists()
public async Task Should_Return_Empty_List_When_No_Books_Exist()
```
## Arrange-Act-Assert (AAA)
```csharp
[Fact]
public async Task Should_Update_Book_Price()
{
// Arrange
var bookId = await CreateTestBookAsync();
var newPrice = 39.99m;
// Act
var result = await _bookAppService.UpdateAsync(bookId, new UpdateBookDto
{
Price = newPrice
});
// Assert
result.Price.ShouldBe(newPrice);
}
```
## Assertions with Shouldly
ABP uses Shouldly library:
```csharp
result.ShouldNotBeNull();
result.Name.ShouldBe("Expected Name");
result.Price.ShouldBeGreaterThan(0);
result.Items.ShouldContain(x => x.Id == expectedId);
result.Items.ShouldBeEmpty();
result.Items.Count.ShouldBe(5);
// Exception assertions
await Should.ThrowAsync<BusinessException>(async () =>
{
await _service.DoSomethingAsync();
});
var ex = await Should.ThrowAsync<BusinessException>(async () =>
{
await _service.DoSomethingAsync();
});
ex.Code.ShouldBe("MyProject:ErrorCode");
```
## Test Data Seeding
```csharp
public class MyProjectTestDataSeedContributor : IDataSeedContributor, ITransientDependency
{
public static readonly Guid TestBookId = Guid.Parse("...");
private readonly IBookRepository _bookRepository;
private readonly IGuidGenerator _guidGenerator;
public async Task SeedAsync(DataSeedContext context)
{
await _bookRepository.InsertAsync(
new Book(TestBookId, "Test Book", 19.99m, Guid.Empty),
autoSave: true
);
}
}
```
## Disabling Authorization in Tests
```csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAlwaysAllowAuthorization();
}
```
## Mocking External Services
Use NSubstitute when needed:
```csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
var emailSender = Substitute.For<IEmailSender>();
emailSender.SendAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(Task.CompletedTask);
context.Services.AddSingleton(emailSender);
}
```
## Testing with Specific User
```csharp
[Fact]
public async Task Should_Get_Current_User_Books()
{
// Login as specific user
await WithUnitOfWorkAsync(async () =>
{
using (CurrentUser.Change(TestData.UserId))
{
var result = await _bookAppService.GetMyBooksAsync();
result.Items.ShouldAllBe(b => b.CreatorId == TestData.UserId);
}
});
}
```
## Testing Multi-Tenancy
```csharp
[Fact]
public async Task Should_Filter_Books_By_Tenant()
{
using (CurrentTenant.Change(TestData.TenantId))
{
var result = await _bookAppService.GetListAsync(new GetBookListDto());
// Results should be filtered by tenant
}
}
```
## Best Practices
- Each test should be independent
- Don't share state between tests
- Use meaningful test data
- Test edge cases and error conditions
- Keep tests focused on single behavior
- Use test data seeders for common data
- Avoid testing framework internals