Project Structure Best Practices
Recommended Solution Structure
MyProject/
├── src/ # 主程式碼目錄
│ └── MyProject.Core/
│ ├── MyProject.Core.csproj
│ ├── Calculator.cs
│ ├── Services/
│ └── Models/
├── tests/ # 測試程式碼目錄
│ └── MyProject.Core.Tests/
│ ├── MyProject.Core.Tests.csproj
│ ├── CalculatorTests.cs
│ ├── Services/
│ └── Models/
└── MyProject.sln
Structural Principles:
- Separation of src and tests: Clearly distinguish production code from test code
- Naming Conventions: The test project name is
{主專案名稱}.Tests - Directory Correspondence: The folder structure of the test project should correspond to the structure of the main project
- One-to-One Mapping: Each main project should have a corresponding test project
Creating an xUnit Test Project
Method 1: Using the .NET CLI (Recommended)
Step 1: Create the solution and project
# 建立解決方案
dotnet new sln -n MyProject
# 建立主專案(類別庫)
dotnet new classlib -n MyProject.Core -o src/MyProject.Core
# 建立測試專案(xUnit 範本)
dotnet new xunit -n MyProject.Core.Tests -o tests/MyProject.Core.Tests
# 將專案加入解決方案
dotnet sln add src/MyProject.Core/MyProject.Core.csproj
dotnet sln add tests/MyProject.Core.Tests/MyProject.Core.Tests.csproj
# 建立專案參考(測試專案參考主專案)
dotnet add tests/MyProject.Core.Tests/MyProject.Core.Tests.csproj reference src/MyProject.Core/MyProject.Core.csproj
Step 2: Install the code coverage tool
# 切換到測試專案目錄
cd tests/MyProject.Core.Tests
# 安裝 coverlet.collector(用於收集程式碼覆蓋率)
dotnet add package coverlet.collector
Method 2: Using Visual Studio
-
Create a solution
- File → New → Project
- Select "Blank Solution"
- Name it after the project name
-
Add the main project
- Right-click the solution → Add → New Project
- Select "Class Library"
- Name it
MyProject.Core
-
Add the test project
- Right-click the solution → Add → New Project
- Search for and select "xUnit Test Project"
- Name it
MyProject.Core.Tests
-
Set up project references
- Right-click the test project → Add → Project Reference
- Check the main project
csproj Configuration for an xUnit Test Project
Standard xUnit test project csproj
Please refer to the templates/xunit-test-project.csproj template file in the same directory.
Core Dependency Package Descriptions:
-
xunit(2.9.3+)
- The core package of the xUnit testing framework
- Provides
[Fact]、[Theory]and other test attributes - Includes the
Assertclass and assertion methods
-
xunit.runner.visualstudio(3.0.0+)
- Visual Studio Test Explorer integration
- Allows tests to be discovered and run in VS Code, Visual Studio, and Rider
- Supports real-time display of test results
-
Microsoft.NET.Test.Sdk(18.3.0+)
- The SDK for the .NET testing platform
- Allows the
dotnet testcommand to run tests - Supports test result reporting and test discovery
-
coverlet.collector(8.0.0+)
- Code coverage collection tool
- Integrates with
dotnet testintegration - Generates coverage reports (supports formats such as Cobertura, OpenCover, etc.)
Important csproj Configuration Items
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
Configuration Descriptions:
IsPackable=false: The test project should not be packaged into a NuGet packageIsTestProject=true: Explicitly mark it as a test project so tools can identify itNullable=enable: Enable nullable reference type checking
Basic Structure of a Test Class
Standard test class template
namespace MyProject.Core.Tests;
public class CalculatorTests
{
private readonly Calculator _calculator;
// 建構函式:每個測試執行前都會被呼叫
public CalculatorTests()
{
_calculator = new Calculator();
}
}
Test Lifecycle (Important Concept)
xUnit's test isolation mechanism:
- Each test method creates a new instance of the test class
- Constructor: Called before each test method is executed
- Test method:Execute test logic
- Dispose():If the implementation
IDisposable,is called after each test method executes
Execution order example:
執行 Test1:
→ 建構函式 → Test1 方法 → Dispose()
執行 Test2:
→ 建構函式 → Test2 方法 → Dispose()
This ensures test isolation,consistent with the FIRST principles of I (Independent)。
Run tests
Using the .NET CLI
# 建置專案
dotnet build
# 執行所有測試
dotnet test
# 執行測試並收集程式碼覆蓋率
dotnet test --collect:"XPlat Code Coverage"
# 執行特定測試專案
dotnet test tests/MyProject.Core.Tests/MyProject.Core.Tests.csproj
# 執行測試並產生詳細輸出
dotnet test --verbosity detailed
Run in the IDE
VS Code (requires the C# Dev Kit)
- Install C# Dev Kit
- Open Test Explorer
- Click the play button next to a test to run it
Visual Studio
- Open Test Explorer (Test → Test Explorer)
- Click "Run All" to run all tests
- You can right-click a single test to run or debug it
JetBrains Rider
- A green run icon appears next to the test method
- Click to run or debug the test
- Use the Unit Tests window to manage tests
Project reference setup principles
Reference direction rules
測試專案 → 主專案 (正確)
主專案 → 測試專案 (錯誤)
The test project should reference the main project, but the main project should absolutely never reference the test project.
Set up project references
# 讓測試專案參考主專案
dotnet add tests/MyProject.Core.Tests/MyProject.Core.Tests.csproj reference src/MyProject.Core/MyProject.Core.csproj
This is generated in the csproj:
<ItemGroup>
<ProjectReference Include="..\..\src\MyProject.Core\MyProject.Core.csproj" />
</ItemGroup>
Advanced: Organizing multiple test projects
As the project grows, you may need multiple test projects:
MyProject/
├── src/
│ ├── MyProject.Core/
│ ├── MyProject.Web/
│ └── MyProject.Infrastructure/
├── tests/
│ ├── MyProject.Core.Tests/ # 單元測試
│ ├── MyProject.Web.Tests/ # Web 層測試
│ ├── MyProject.Infrastructure.Tests/ # 基礎設施測試
│ └── MyProject.Integration.Tests/ # 整合測試
└── MyProject.sln
Naming convention recommendations:
*.Tests- Unit tests*.Integration.Tests- Integration tests*.Acceptance.Tests- Acceptance tests*.Performance.Tests- Performance tests
Naming conventions for real-world work projects
In real-world work projects, it is recommended to use a clearer naming format to distinguish test types:
Recommended naming format:
MyProject/
├── src/
│ ├── MyProject.Core/
│ └── MyProject.WebApi/
├── tests/
│ ├── MyProject.Core.Test.Unit/ # 單元測試(明確標示)
│ ├── MyProject.WebApi.Test.Unit/ # WebApi 單元測試
│ └── MyProject.WebApi.Test.Integration/ # WebApi 整合測試
└── MyProject.sln
Naming rules:
-
Unit tests:
{專案名稱}.Test.Unit- Example:
MyProject.Core.Test.Unit - Characteristics: Does not depend on external resources (databases, APIs, file systems, etc.)
- Execution speed: Fast (millisecond level)
- Example:
-
Integration tests:
{專案名稱}.Test.Integration- Example:
MyProject.WebApi.Test.Integration - Characteristics: Tests the integration of multiple components and may depend on external resources
- Execution speed: Slower (second level)
- Example:
Advantages of this naming:
-
Clarity: You can tell the test type at a glance
-
Execution strategy: Can be executed in stages in CI/CD
# 快速回饋:只執行單元測試 dotnet test --filter "FullyQualifiedName~.Test.Unit" # 完整驗證:執行整合測試 dotnet test --filter "FullyQualifiedName~.Test.Integration" -
Dependency management: Integration tests can have different package dependencies (such as Testcontainers)
-
Team collaboration: New members can quickly understand the project structure
CLI creation example:
# 建立單元測試專案
dotnet new xunit -n MyProject.Core.Test.Unit -o tests/MyProject.Core.Test.Unit
dotnet add tests/MyProject.Core.Test.Unit reference src/MyProject.Core
# 建立整合測試專案
dotnet new xunit -n MyProject.WebApi.Test.Integration -o tests/MyProject.WebApi.Test.Integration
dotnet add tests/MyProject.WebApi.Test.Integration reference src/MyProject.WebApi
Tip: Although this example uses
.Testsformat to simplify the explanation, in real projects it is strongly recommended to use.Test.Unitand.Test.Integrationthis clearer format.
Common issues and solutions
Q1: Test discovery fails, and Test Explorer cannot see the tests?
Checklist:
- Confirm that
xunit.runner.visualstudiopackage is installed - Confirm that
Microsoft.NET.Test.Sdkpackage is installed - Run
dotnet buildto rebuild - Restart the IDE or reload Test Explorer
Q2: Tests can run in the CLI but cannot run in the IDE?
Solution:
- Verify that the IDE has installed the relevant extensions (VS Code requires the C# Dev Kit)
- Clear the cache: delete the
bin/andobj/folders, then rebuild - Check whether the
.csprojproperty inIsTestProjectistrue
Q3: How do I use an Internal class in a test project?
In the main project's .csproj or AssemblyInfo.cs add:
[assembly: InternalsVisibleTo("MyProject.Core.Tests")]
Or in the csproj:
<ItemGroup>
<InternalsVisibleTo Include="MyProject.Core.Tests" />
</ItemGroup>
Template Files
Please refer to the template files in the same directory to quickly create a project:
templates/project-structure.md- Complete example of the project structuretemplates/xunit-test-project.csproj- csproj template for an xUnit test project
Checklist
When creating an xUnit test project, please confirm the following items:
- The test project is named
{主專案名稱}.Tests - The test project is located in the
tests/directory - Installed the
xunit、xunit.runner.visualstudio、Microsoft.NET.Test.Sdkpackage - Installed
coverlet.collectorfor code coverage - The test project references the main project
-
IsPackableSet tofalse -
IsTestProjectSet totrue - Can run
dotnet testsuccessfully - The IDE's Test Explorer can discover the tests
Output Format
- Generate a complete xUnit test project structure (src/ and tests/ separated)
- Generate the test project csproj file (including the required package references)
- Provide the dotnet CLI command sequence for creation
- Include the project reference settings and xunit.runner.json configuration (if needed)
- Generate a basic test class template (.cs file)
Reference Resources
Original Article
The content of this skill is distilled from the "Old-School Software Engineer's Testing Training - 30-Day Challenge" article series:
-
Day 02 - In-Depth Analysis of the xUnit Framework
-
Day 03 - Advanced xUnit Features and Test Data Management
Official Documentation
Related Skills
unit-test-fundamentals- Unit Testing Basics and the FIRST Principlestest-naming-conventions- Test Naming Conventionscode-coverage-analysis- Code Coverage Analysis