NUnit Testing in ASP.NET CORE MC
Let’s implement NUnit unit testing for an Employee Management System in ASP.NET Core MVC, incorporating a service layer and repository layer, and setting it up entirely within Visual Studio 2022 (without CLI commands).
I’ll provide a step-by-step explanation with source code, comments, proper naming conventions, coding standards, and test results for the use case involving Employee, Department, Project, and Task.
We’ll focus on what each part does, why it’s used, and how to configure it in Visual Studio 2022.
Step 1: Understand the Setup
What
- Build an MVC project with a service layer (business logic), repository layer (data access), and a test project using NUnit in Visual Studio 2022.
Why
- Service Layer: Encapsulates business logic (e.g., assigning tasks), testable independently of data access.
- Repository Layer: Abstracts data operations (e.g., in-memory or database), making tests mockable.
- NUnit: Validates functionality, ensuring the system works as expected (e.g., task assignment in an Employee Management System).
Use Case
- Entities: Employee, Department, Project, Task.
- Focus: Test task assignment and retrieval for employees.
Step 2: Create Projects in Visual Studio 2022
How
- Open Visual Studio 2022:
- Launch VS 2022, choose "Create a new project."
- Create MVC Project:
- Search for "ASP.NET Core Web App (Model-View-Controller)".
- Name: EmployeeManagement.
- Location: Choose a folder (e.g., C:\Projects).
- Framework: .NET 8 (latest as of March 2025).
- Click "Create."
- Add Test Project:
- In Solution Explorer, right-click the solution → "Add" → "New Project".
- Search for "NUnit Test Project (.NET Core)".
- Name: EmployeeManagement.Tests.
- Location: Same as above.
- Framework: .NET 8.
- Click "Create."
- Add Project Reference:
- In Solution Explorer, right-click EmployeeManagement.Tests → "Add" → "Project Reference".
- Check EmployeeManagement, click "OK."
Why
- Visual Studio simplifies project creation with templates, avoiding CLI complexity.
- Separate test project keeps production and test code isolated.
Step 3: Define Models
What
- Define entity classes for the system.
How
- In EmployeeManagement, right-click "Models" folder → "Add" → "Class".
- Create each file:
- EmployeeManagement/Models/Employee.cs:csharp
// Represents an employee in the system public class Employee { public int EmployeeId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public int DepartmentId { get; set; } public Department Department { get; set; } }
- EmployeeManagement/Models/Department.cs:csharp
// Represents a department in the organization public class Department { public int DepartmentId { get; set; } public string DepartmentName { get; set; } }
- EmployeeManagement/Models/Project.cs:csharp
// Represents a project that employees work on public class Project { public int ProjectId { get; set; } public string ProjectName { get; set; } }
- EmployeeManagement/Models/Task.cs:csharp
// Represents a task assigned to an employee in a project public class Task { public int TaskId { get; set; } public string TaskName { get; set; } public int EmployeeId { get; set; } public int ProjectId { get; set; } public DateTime StartTime { get; set; } public DateTime EndTime { get; set; } }
Why
- Models define the system’s core entities, enabling testing of operations like task assignment.
Naming Convention
- PascalCase for classes (Employee) and properties (TaskId).
- Clear, domain-specific names.
Step 4: Implement Repository Layer
What
- Create an interface and in-memory repository for data access.
How
- In EmployeeManagement, add a "Repositories" folder → right-click → "Add" → "Interface".
- Name: ITaskRepository.cs.
- Add a class InMemoryTaskRepository.cs.
- EmployeeManagement/Repositories/ITaskRepository.cs:csharp
public interface ITaskRepository { void AddTask(Task task); List<Task> GetTasksByEmployeeId(int employeeId); }
- EmployeeManagement/Repositories/InMemoryTaskRepository.cs:
public class InMemoryTaskRepository : ITaskRepository { private readonly List<Task> _tasks = new List<Task>(); // Adds a task to the in-memory store public void AddTask(Task task) { _tasks.Add(task); } // Retrieves tasks for a specific employee public List<Task> GetTasksByEmployeeId(int employeeId) { return _tasks.Where(t => t.EmployeeId == employeeId).ToList(); } }
Why
- Repository abstracts data access, allowing tests to use an in-memory implementation instead of a database.
- Interface enables mocking or swapping implementations (e.g., SQL Server later).
Coding Standards
- Interface prefixed with I (ITaskRepository).
- Private field _tasks with underscore prefix.
- Concise, focused methods.
Step 5: Implement Service Layer
What
- Create a service to handle business logic, using the repository.
How
- In EmployeeManagement, add a "Services" folder → right-click → "Add" → "Interface" (IEmployeeService.cs) and "Class" (EmployeeService.cs).
- EmployeeManagement/Services/IEmployeeService.cs:
public interface IEmployeeService { Task AssignTaskToEmployee(int employeeId, int projectId, string taskName, DateTime startTime, DateTime endTime); List<Task> GetTasksForEmployee(int employeeId); }
- EmployeeManagement/Services/EmployeeService.cs:
public class EmployeeService : IEmployeeService { private readonly ITaskRepository _taskRepository; // Constructor injection for repository public EmployeeService(ITaskRepository taskRepository) { _taskRepository = taskRepository ?? throw new ArgumentNullException(nameof(taskRepository)); } // Assigns a task to an employee for a project public Task AssignTaskToEmployee(int employeeId, int projectId, string taskName, DateTime startTime, DateTime endTime) { if (string.IsNullOrWhiteSpace(taskName)) throw new ArgumentException("Task name cannot be empty.", nameof(taskName)); if (endTime <= startTime) throw new ArgumentException("End time must be after start time.", nameof(endTime)); var task = new Task { TaskId = GetNextTaskId(), // Simplified for in-memory EmployeeId = employeeId, ProjectId = projectId, TaskName = taskName, StartTime = startTime, EndTime = endTime }; _taskRepository.AddTask(task); return task; } // Retrieves all tasks for a given employee public List<Task> GetTasksForEmployee(int employeeId) { return _taskRepository.GetTasksByEmployeeId(employeeId); } // Helper method to simulate auto-incrementing ID private int GetNextTaskId() { var tasks = _taskRepository.GetTasksByEmployeeId(0); // All tasks return tasks.Count > 0 ? tasks.Max(t => t.TaskId) + 1 : 1; } }
Why
- Service layer contains business rules (e.g., validation), testable independently of controllers or UI.
- Dependency injection (ITaskRepository) promotes loose coupling.
Coding Standards
- Constructor validation for dependencies.
- Descriptive method names (AssignTaskToEmployee).
- Consistent spacing and bracing.
Step 6: Configure Dependency Injection
What
- Register services in the MVC project.
How
- Open EmployeeManagement/Program.cs:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllersWithViews(); // Register dependencies builder.Services.AddScoped<ITaskRepository, InMemoryTaskRepository>(); builder.Services.AddScoped<IEmployeeService, EmployeeService>(); var app = builder.Build(); app.UseStaticFiles(); app.UseRouting(); app.UseCookiePolicy(); app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}"); app.Run();
Why
- Dependency injection ensures testable, modular code by providing instances at runtime.
Step 7: Write NUnit Tests
What
- Create tests for EmployeeService using NUnit in Visual Studio 2022.
How
- In EmployeeManagement.Tests, rename the default UnitTest1.cs to EmployeeServiceTests.cs (right-click → "Rename").
- EmployeeManagement.Tests/EmployeeServiceTests.cs:
using NUnit.Framework; using EmployeeManagement.Services; using EmployeeManagement.Models; using EmployeeManagement.Repositories; using System; using System.Collections.Generic; namespace EmployeeManagement.Tests { [TestFixture] public class EmployeeServiceTests { private IEmployeeService _employeeService; private ITaskRepository _taskRepository; // Sets up test dependencies before each test [SetUp] public void Setup() { _taskRepository = new InMemoryTaskRepository(); _employeeService = new EmployeeService(_taskRepository); } [Test] public void AssignTaskToEmployee_ValidInput_ReturnsAssignedTask() { // Arrange int employeeId = 1; int projectId = 1; string taskName = "Design Homepage"; DateTime startTime = new DateTime(2025, 3, 1, 9, 0, 0); DateTime endTime = new DateTime(2025, 3, 1, 17, 0, 0); // Act var result = _employeeService.AssignTaskToEmployee(employeeId, projectId, taskName, startTime, endTime); // Assert Assert.IsNotNull(result, "Task should not be null."); Assert.AreEqual(employeeId, result.EmployeeId, "EmployeeId should match."); Assert.AreEqual(projectId, result.ProjectId, "ProjectId should match."); Assert.AreEqual(taskName, result.TaskName, "TaskName should match."); } [Test] public void AssignTaskToEmployee_InvalidTime_ThrowsArgumentException() { // Arrange int employeeId = 1; int projectId = 1; string taskName = "Invalid Task"; DateTime startTime = new DateTime(2025, 3, 1, 17, 0, 0); DateTime endTime = new DateTime(2025, 3, 1, 9, 0, 0); // End before start // Act & Assert var exception = Assert.Throws<ArgumentException>(() => _employeeService.AssignTaskToEmployee(employeeId, projectId, taskName, startTime, endTime)); Assert.AreEqual("End time must be after start time.", exception.Message); } [Test] public void GetTasksForEmployee_WithTasks_ReturnsCorrectTasks() { // Arrange int employeeId = 2; _employeeService.AssignTaskToEmployee(employeeId, 1, "Task 1", DateTime.Now, DateTime.Now.AddHours(1)); _employeeService.AssignTaskToEmployee(employeeId, 2, "Task 2", DateTime.Now, DateTime.Now.AddHours(2)); _employeeService.AssignTaskToEmployee(3, 1, "Task 3", DateTime.Now, DateTime.Now.AddHours(1)); // Act var tasks = _employeeService.GetTasksForEmployee(employeeId); // Assert Assert.AreEqual(2, tasks.Count, "Should return 2 tasks."); Assert.IsTrue(tasks.All(t => t.EmployeeId == employeeId), "All tasks should belong to the employee."); } [Test] public void GetTasksForEmployee_NoTasks_ReturnsEmptyList() { // Arrange int employeeId = 4; // Act var tasks = _employeeService.GetTasksForEmployee(employeeId); // Assert Assert.IsEmpty(tasks, "Should return an empty list."); } } }
Why
- Tests verify EmployeeService logic through the repository, ensuring correct task management.
- Setup ensures isolation per test.
Naming Convention
- Class: EmployeeServiceTests (service name + “Tests”).
- Methods: [Action]_[Condition]_[ExpectedResult] (e.g., GetTasksForEmployee_WithTasks_ReturnsCorrectTasks).
Coding Standards
- AAA pattern (Arrange, Act, Assert).
- Descriptive comments at key points.
- Consistent indentation and spacing.
Step 8: Run Tests in Visual Studio 2022
How
- Open Test Explorer:
- Go to "Test" → "Test Explorer" in the VS 2022 menu.
- Build Solution:
- Press Ctrl+Shift+B or "Build" → "Build Solution."
- Run Tests:
- In Test Explorer, right-click EmployeeServiceTests → "Run" (or click the green "Run All" button).
Test Results
- Output (assuming all pass):
Passed: AssignTaskToEmployee_ValidInput_ReturnsAssignedTask Passed: AssignTaskToEmployee_InvalidTime_ThrowsArgumentException Passed: GetTasksForEmployee_WithTasks_ReturnsCorrectTasks Passed: GetTasksForEmployee_NoTasks_ReturnsEmptyList Total: 4, Passed: 4, Failed: 0, Skipped: 0
- Details:
- ValidInput: Task assigned correctly.
- InvalidTime: Exception thrown as expected.
- WithTasks: Returns 2 tasks for employee 2.
- NoTasks: Returns empty list.
Why
- Visual Studio’s Test Explorer provides a GUI for running and debugging tests, ideal for development workflows.
Step 9: Test Cases Summary
Test Case | Description | Input | Expected Output | Result |
---|---|---|---|---|
AssignTaskToEmployee_ValidInput_ReturnsAssignedTask | Assign a valid task | EmpId=1, ProjId=1, "Design", valid times | Task with correct properties | Pass |
AssignTaskToEmployee_InvalidTime_ThrowsArgumentException | Assign task with invalid time | EmpId=1, ProjId=1, "Task", end < start | ArgumentException: "End time..." | Pass |
GetTasksForEmployee_WithTasks_ReturnsCorrectTasks | Retrieve tasks for employee with tasks | EmpId=2 (2 tasks added) | List with 2 tasks | Pass |
GetTasksForEmployee_NoTasks_ReturnsEmptyList | Retrieve tasks for employee with no tasks | EmpId=4 (no tasks) | Empty list | Pass |
Conclusion
- What: Added service and repository layers, tested with NUnit in Visual Studio 2022.
- Why: Ensures modular, testable code for the Employee Management System.
- Setup: Done via VS 2022 GUI, avoiding CLI for your preference.
This setup is ready for expansion (e.g., SQL repository, more tests).
Comments
Post a Comment