Initial commit
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
using RobotNet10.FleetManager.Data;
|
||||
using RobotNet10.FleetManager.Shared.Enums;
|
||||
|
||||
namespace RobotNet10.RobotManager.Test.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Helper methods for creating test data
|
||||
/// </summary>
|
||||
public static class TestHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a test RobotModel entity
|
||||
/// </summary>
|
||||
public static RobotModel CreateTestRobotModel(
|
||||
Guid? id = null,
|
||||
string? modelName = null,
|
||||
double length = 1.0,
|
||||
double width = 0.5,
|
||||
int imageWidth = 800,
|
||||
int imageHeight = 600,
|
||||
double navigationPointX = 0.0,
|
||||
double navigationPointY = 0.0,
|
||||
NavigationType navigationType = NavigationType.Differential)
|
||||
{
|
||||
return new RobotModel
|
||||
{
|
||||
Id = id ?? Guid.NewGuid(),
|
||||
ModelName = modelName ?? "TestModel",
|
||||
Length = length,
|
||||
Width = width,
|
||||
ImageWidth = imageWidth,
|
||||
ImageHeight = imageHeight,
|
||||
NavigationPointX = navigationPointX,
|
||||
NavigationPointY = navigationPointY,
|
||||
NavigationType = navigationType,
|
||||
CreatedDate = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a test Robot entity
|
||||
/// </summary>
|
||||
public static Robot CreateTestRobot(
|
||||
Guid? id = null,
|
||||
string? robotId = null,
|
||||
string? name = null,
|
||||
Guid? modelId = null,
|
||||
Guid? mapId = null)
|
||||
{
|
||||
return new Robot
|
||||
{
|
||||
Id = id ?? Guid.NewGuid(),
|
||||
RobotId = robotId ?? "ROBOT-001",
|
||||
Name = name ?? "Test Robot",
|
||||
ModelId = modelId ?? Guid.NewGuid(),
|
||||
MapId = mapId,
|
||||
CreatedDate = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seeds test data into the database context
|
||||
/// </summary>
|
||||
public static void SeedTestData(ApplicationDbContext context)
|
||||
{
|
||||
var model1 = CreateTestRobotModel(
|
||||
id: Guid.Parse("11111111-1111-1111-1111-111111111111"),
|
||||
modelName: "AMR-T800",
|
||||
length: 1.2,
|
||||
width: 0.8);
|
||||
|
||||
var model2 = CreateTestRobotModel(
|
||||
id: Guid.Parse("22222222-2222-2222-2222-222222222222"),
|
||||
modelName: "AMR-F500",
|
||||
length: 1.0,
|
||||
width: 0.6,
|
||||
navigationType: NavigationType.Forklift);
|
||||
|
||||
context.RobotModels.AddRange(model1, model2);
|
||||
|
||||
var robot1 = CreateTestRobot(
|
||||
id: Guid.Parse("33333333-3333-3333-3333-333333333333"),
|
||||
robotId: "ROBOT-001",
|
||||
name: "Robot 1",
|
||||
modelId: model1.Id);
|
||||
|
||||
var robot2 = CreateTestRobot(
|
||||
id: Guid.Parse("44444444-4444-4444-4444-444444444444"),
|
||||
robotId: "ROBOT-002",
|
||||
name: "Robot 2",
|
||||
modelId: model1.Id);
|
||||
|
||||
var robot3 = CreateTestRobot(
|
||||
id: Guid.Parse("55555555-5555-5555-5555-555555555555"),
|
||||
robotId: "ROBOT-003",
|
||||
name: "Robot 3",
|
||||
modelId: model2.Id);
|
||||
|
||||
context.Robots.AddRange(robot1, robot2, robot3);
|
||||
context.SaveChanges();
|
||||
}
|
||||
}
|
||||
144
srcs/RobotNet10/Tests/RobotNet10.RobotManager.Test/README.md
Normal file
144
srcs/RobotNet10/Tests/RobotNet10.RobotManager.Test/README.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# Unit Tests - RobotNet10.RobotManager
|
||||
|
||||
Thư mục này chứa các unit tests cho Robot Management module.
|
||||
|
||||
## 📁 Cấu trúc Tests
|
||||
|
||||
```
|
||||
RobotNet10.RobotManager.Test/
|
||||
├── Helpers/
|
||||
│ └── TestHelpers.cs # Helper methods để tạo test data
|
||||
├── Services/
|
||||
│ ├── RobotModelServiceTests.cs # Tests cho RobotModelService
|
||||
│ ├── RobotServiceTests.cs # Tests cho RobotService
|
||||
│ └── RobotModelImageStorageServiceTests.cs # Tests cho Image Storage Service
|
||||
├── README.md # File này
|
||||
└── RobotNet10.RobotManager.Test.csproj
|
||||
```
|
||||
|
||||
## 🧪 Test Coverage
|
||||
|
||||
### ✅ Đã được test
|
||||
|
||||
1. **RobotModelService**
|
||||
- CreateAsync với valid data
|
||||
- CreateAsync với duplicate model name (throws exception)
|
||||
- GetAllAsync
|
||||
- GetByIdAsync (existing và non-existent)
|
||||
- UpdateAsync (valid data và non-existent ID)
|
||||
- DeleteAsync (existing, non-existent, và model với robots)
|
||||
- SearchAsync
|
||||
- ExistsAsync
|
||||
- GetUsageInfoAsync
|
||||
|
||||
2. **RobotService**
|
||||
- CreateAsync với valid data
|
||||
- CreateAsync với duplicate robot ID (throws exception)
|
||||
- CreateAsync với non-existent model ID (throws exception)
|
||||
- GetAllAsync (với và không có filters)
|
||||
- GetByIdAsync
|
||||
- GetByRobotIdAsync
|
||||
- UpdateAsync
|
||||
- DeleteAsync
|
||||
- GetByModelIdAsync
|
||||
- SearchAsync
|
||||
- ExistsAsync
|
||||
|
||||
3. **RobotModelImageStorageService**
|
||||
- SaveImageAsync
|
||||
- GetImageAsync
|
||||
- DeleteImageAsync
|
||||
- ImageExistsAsync
|
||||
- GetImageDimensionsAsync
|
||||
|
||||
## 🚀 Chạy Tests
|
||||
|
||||
### Sử dụng Visual Studio
|
||||
1. Mở Test Explorer (Test → Test Explorer)
|
||||
2. Build solution
|
||||
3. Chọn tests cần chạy và click "Run"
|
||||
|
||||
### Sử dụng dotnet CLI
|
||||
```bash
|
||||
cd srcs/RobotNet10/Tests/RobotNet10.RobotManager.Test
|
||||
dotnet test
|
||||
```
|
||||
|
||||
### Chạy tests cụ thể
|
||||
```bash
|
||||
# Chạy tests trong một class
|
||||
dotnet test --filter "FullyQualifiedName~RobotModelServiceTests"
|
||||
|
||||
# Chạy một test method cụ thể
|
||||
dotnet test --filter "FullyQualifiedName~CreateAsync_ValidData_ReturnsCreatedRobotModel"
|
||||
```
|
||||
|
||||
### Chạy với output chi tiết
|
||||
```bash
|
||||
dotnet test --verbosity normal
|
||||
```
|
||||
|
||||
### Chạy với code coverage
|
||||
```bash
|
||||
dotnet test --collect:"XPlat Code Coverage"
|
||||
```
|
||||
|
||||
## 📊 Test Statistics
|
||||
|
||||
- **Total Tests**: ~30+ test cases
|
||||
- **Test Framework**: NUnit
|
||||
- **Coverage Areas**:
|
||||
- RobotModelService: CRUD operations, validation, business rules
|
||||
- RobotService: CRUD operations, validation, filtering
|
||||
- RobotModelImageStorageService: File operations
|
||||
|
||||
## 🛠️ Test Helpers
|
||||
|
||||
`TestHelpers.cs` cung cấp các helper methods để tạo test data:
|
||||
|
||||
- `CreateTestRobotModel()` - Tạo RobotModel entity với các parameters tùy chọn
|
||||
- `CreateTestRobot()` - Tạo Robot entity với các parameters tùy chọn
|
||||
- `SeedTestData()` - Seed test data vào database context
|
||||
|
||||
## 📝 Best Practices
|
||||
|
||||
1. **AAA Pattern**: Arrange, Act, Assert
|
||||
2. **Test Isolation**: Mỗi test độc lập, sử dụng InMemory database riêng
|
||||
3. **Meaningful Names**: Tên test mô tả rõ behavior được test
|
||||
4. **Test Data**: Sử dụng TestHelpers để tạo consistent test data
|
||||
5. **Edge Cases**: Test cả success và failure scenarios
|
||||
|
||||
## 🔍 Test Categories
|
||||
|
||||
### Unit Tests
|
||||
- Test từng service riêng biệt
|
||||
- Sử dụng InMemory database để test database operations
|
||||
- Mock external dependencies (IWebHostEnvironment, ILogger)
|
||||
- Fast execution
|
||||
|
||||
### Integration Tests (Future)
|
||||
- Test interaction giữa các components
|
||||
- Test với real database (optional)
|
||||
- Test end-to-end scenarios
|
||||
|
||||
## 📈 Coverage Goals
|
||||
|
||||
- [x] RobotModelService: Core methods ~90%
|
||||
- [x] RobotService: Core methods ~90%
|
||||
- [x] RobotModelImageStorageService: File operations ~80%
|
||||
- [ ] Controllers: Pending (có thể thêm sau)
|
||||
- [ ] SignalR Hub: Pending (cần integration test)
|
||||
|
||||
## 🐛 Known Issues
|
||||
|
||||
- Image dimension tests sử dụng invalid image data (cần valid PNG để test đầy đủ)
|
||||
- Controller tests chưa được implement (có thể thêm sau với integration tests)
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Khi thêm tests mới:
|
||||
|
||||
1. Follow naming convention: `MethodName_Scenario_ExpectedBehavior`
|
||||
2. Sử dụng TestHelpers cho test data
|
||||
3. Đảm bảo test isolation (mỗi test có database riêng)
|
||||
4. Test cả success và failure cases
|
||||
@@ -0,0 +1,37 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="8.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.3" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="NUnit" Version="4.5.0" />
|
||||
<PackageReference Include="NUnit.Analyzers" Version="4.11.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\FleetManager\RobotNet10.FleetManager\RobotNet10.FleetManager.csproj" />
|
||||
<ProjectReference Include="..\..\FleetManager\RobotNet10.FleetManager.Shared\RobotNet10.FleetManager.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="NUnit.Framework" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,176 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using RobotNet10.FleetManager.Services;
|
||||
using RobotNet10.StorageManager;
|
||||
using System.Text;
|
||||
|
||||
namespace RobotNet10.RobotManager.Test.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for RobotModelImageStorageService
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class RobotModelImageStorageServiceTests
|
||||
{
|
||||
private string _testDirectory = null!;
|
||||
private RobotModelImageStorageService _service = null!;
|
||||
private IOptionsMonitor<StorageConfig> _optionsMonitor = null!;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
// Create a temporary test directory
|
||||
_testDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(_testDirectory);
|
||||
|
||||
// Create StorageConfig for local storage
|
||||
var storageConfig = new StorageConfig
|
||||
{
|
||||
UsingLocal = true,
|
||||
LocalFolder = _testDirectory,
|
||||
RetryCount = 3
|
||||
};
|
||||
|
||||
// Mock IOptionsMonitor<StorageConfig>
|
||||
var mockOptionsMonitor = new Moq.Mock<IOptionsMonitor<StorageConfig>>();
|
||||
mockOptionsMonitor.Setup(m => m.Get("RobotModelImages")).Returns(storageConfig);
|
||||
|
||||
_optionsMonitor = mockOptionsMonitor.Object;
|
||||
var logger = new LoggerFactory().CreateLogger<RobotModelImageStorageService>();
|
||||
_service = new RobotModelImageStorageService(_optionsMonitor, logger);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
// Dispose service to clean up StorageManager
|
||||
_service?.Dispose();
|
||||
|
||||
// Clean up test directory
|
||||
if (Directory.Exists(_testDirectory))
|
||||
{
|
||||
Directory.Delete(_testDirectory, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SaveImageAsync_ValidImage_SavesToFileSystem()
|
||||
{
|
||||
// Arrange
|
||||
var robotModelId = Guid.NewGuid();
|
||||
var imageBytes = Encoding.UTF8.GetBytes("Fake PNG image data");
|
||||
using var imageStream = new MemoryStream(imageBytes);
|
||||
|
||||
// Act
|
||||
await _service.SaveImageAsync(robotModelId, imageStream);
|
||||
|
||||
// Assert
|
||||
// StorageManager stores files in path "robotModelImages" with objectName = robotModelId
|
||||
var expectedPath = Path.Combine(_testDirectory, "robotModelImages", $"{robotModelId}.png");
|
||||
Assert.That(File.Exists(expectedPath), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetImageAsync_ExistingImage_ReturnsStream()
|
||||
{
|
||||
// Arrange
|
||||
var robotModelId = Guid.NewGuid();
|
||||
var imageBytes = Encoding.UTF8.GetBytes("Fake PNG image data");
|
||||
using var saveStream = new MemoryStream(imageBytes);
|
||||
await _service.SaveImageAsync(robotModelId, saveStream);
|
||||
|
||||
// Act
|
||||
using var result = await _service.GetImageAsync(robotModelId);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result, Is.InstanceOf<Stream>());
|
||||
Assert.That(result!.Length, Is.GreaterThan(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetImageAsync_NonExistentImage_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var nonExistentId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var result = await _service.GetImageAsync(nonExistentId);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DeleteImageAsync_ExistingImage_DeletesFile()
|
||||
{
|
||||
// Arrange
|
||||
var robotModelId = Guid.NewGuid();
|
||||
var imageBytes = Encoding.UTF8.GetBytes("Fake PNG image data");
|
||||
using var saveStream = new MemoryStream(imageBytes);
|
||||
await _service.SaveImageAsync(robotModelId, saveStream);
|
||||
|
||||
// Act
|
||||
await _service.DeleteImageAsync(robotModelId);
|
||||
|
||||
// Assert
|
||||
// StorageManager stores files in path "robotModelImages" with objectName = robotModelId
|
||||
var expectedPath = Path.Combine(_testDirectory, "robotModelImages", $"{robotModelId}.png");
|
||||
Assert.That(File.Exists(expectedPath), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DeleteImageAsync_NonExistentImage_DoesNotThrow()
|
||||
{
|
||||
// Arrange
|
||||
var nonExistentId = Guid.NewGuid();
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
Assert.DoesNotThrowAsync(async () => await _service.DeleteImageAsync(nonExistentId));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ImageExistsAsync_ExistingImage_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var robotModelId = Guid.NewGuid();
|
||||
var imageBytes = Encoding.UTF8.GetBytes("Fake PNG image data");
|
||||
using var saveStream = new MemoryStream(imageBytes);
|
||||
await _service.SaveImageAsync(robotModelId, saveStream);
|
||||
|
||||
// Act
|
||||
var result = await _service.ImageExistsAsync(robotModelId);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ImageExistsAsync_NonExistentImage_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var nonExistentId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var result = await _service.ImageExistsAsync(nonExistentId);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetImageDimensionsAsync_InvalidImageStream_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
// Note: This test uses invalid image data to verify error handling
|
||||
var imageBytes = Encoding.UTF8.GetBytes("Invalid image data");
|
||||
using var imageStream = new MemoryStream(imageBytes);
|
||||
|
||||
// Act & Assert
|
||||
// Since we're using invalid image data, this should throw InvalidOperationException
|
||||
// (which wraps UnknownImageFormatException from ImageSharp)
|
||||
var ex = Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await _service.GetImageDimensionsAsync(imageStream));
|
||||
Assert.That(ex!.Message, Does.Contain("Invalid image format or corrupted file"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using RobotNet10.FleetManager.Data;
|
||||
using RobotNet10.FleetManager.Services;
|
||||
using RobotNet10.FleetManager.Shared.DTOs.RobotModel;
|
||||
using RobotNet10.FleetManager.Shared.Enums;
|
||||
using RobotNet10.RobotManager.Test.Helpers;
|
||||
|
||||
namespace RobotNet10.RobotManager.Test.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for RobotModelService
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class RobotModelServiceTests
|
||||
{
|
||||
private ApplicationDbContext _context = null!;
|
||||
private RobotModelService _service = null!;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
|
||||
_context = new ApplicationDbContext(options);
|
||||
var logger = new LoggerFactory().CreateLogger<RobotModelService>();
|
||||
var _logger = new FleetManager.Services.Logger<RobotModelService>(logger);
|
||||
_service = new RobotModelService(_context, _logger);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
_context.Dispose();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CreateAsync_ValidData_ReturnsCreatedRobotModel()
|
||||
{
|
||||
// Arrange
|
||||
var request = new CreateRobotModelRequest
|
||||
{
|
||||
ModelName = "TestModel",
|
||||
Length = 1.2,
|
||||
Width = 0.8,
|
||||
ImageWidth = 800,
|
||||
ImageHeight = 600,
|
||||
NavigationPointX = 0.0,
|
||||
NavigationPointY = 0.0,
|
||||
NavigationType = NavigationType.Differential
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _service.CreateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result.ModelName, Is.EqualTo(request.ModelName));
|
||||
Assert.That(result.Length, Is.EqualTo(request.Length));
|
||||
Assert.That(result.Width, Is.EqualTo(request.Width));
|
||||
Assert.That(result.NavigationType, Is.EqualTo(request.NavigationType));
|
||||
Assert.That(result.Id, Is.Not.EqualTo(Guid.Empty));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CreateAsync_DuplicateModelName_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
var existingModel = TestHelpers.CreateTestRobotModel(modelName: "ExistingModel");
|
||||
_context.RobotModels.Add(existingModel);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var request = new CreateRobotModelRequest
|
||||
{
|
||||
ModelName = "ExistingModel",
|
||||
Length = 1.0,
|
||||
Width = 0.5,
|
||||
ImageWidth = 800,
|
||||
ImageHeight = 600,
|
||||
NavigationPointX = 0.0,
|
||||
NavigationPointY = 0.0,
|
||||
NavigationType = NavigationType.Differential
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.ThrowsAsync<InvalidOperationException>(async () => await _service.CreateAsync(request));
|
||||
Assert.That(ex!.Message, Does.Contain("already exists"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetAllAsync_ReturnsAllRobotModels()
|
||||
{
|
||||
// Arrange
|
||||
TestHelpers.SeedTestData(_context);
|
||||
|
||||
// Act
|
||||
var result = await _service.GetAllAsync();
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result.Count, Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetByIdAsync_ExistingId_ReturnsRobotModel()
|
||||
{
|
||||
// Arrange
|
||||
var model = TestHelpers.CreateTestRobotModel();
|
||||
_context.RobotModels.Add(model);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await _service.GetByIdAsync(model.Id);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result!.Id, Is.EqualTo(model.Id));
|
||||
Assert.That(result.ModelName, Is.EqualTo(model.ModelName));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetByIdAsync_NonExistentId_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var nonExistentId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var result = await _service.GetByIdAsync(nonExistentId);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task UpdateAsync_ValidData_UpdatesRobotModel()
|
||||
{
|
||||
// Arrange
|
||||
var model = TestHelpers.CreateTestRobotModel();
|
||||
_context.RobotModels.Add(model);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var request = new UpdateRobotModelRequest
|
||||
{
|
||||
ModelName = "UpdatedModel",
|
||||
Length = 2.0,
|
||||
Width = 1.0,
|
||||
ImageWidth = 1000,
|
||||
ImageHeight = 800,
|
||||
NavigationPointX = 0.1,
|
||||
NavigationPointY = 0.2,
|
||||
NavigationType = NavigationType.OmniDrive
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _service.UpdateAsync(model.Id, request);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result.ModelName, Is.EqualTo(request.ModelName));
|
||||
Assert.That(result.Length, Is.EqualTo(request.Length));
|
||||
Assert.That(result.NavigationType, Is.EqualTo(request.NavigationType));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task UpdateAsync_NonExistentId_ThrowsKeyNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
var nonExistentId = Guid.NewGuid();
|
||||
var request = new UpdateRobotModelRequest
|
||||
{
|
||||
ModelName = "Test",
|
||||
Length = 1.0,
|
||||
Width = 0.5,
|
||||
ImageWidth = 800,
|
||||
ImageHeight = 600,
|
||||
NavigationPointX = 0.0,
|
||||
NavigationPointY = 0.0,
|
||||
NavigationType = NavigationType.Differential
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
Assert.ThrowsAsync<KeyNotFoundException>(async () => await _service.UpdateAsync(nonExistentId, request));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DeleteAsync_ExistingId_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var model = TestHelpers.CreateTestRobotModel();
|
||||
_context.RobotModels.Add(model);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await _service.DeleteAsync(model.Id);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.True);
|
||||
var deleted = await _context.RobotModels.FindAsync(model.Id);
|
||||
Assert.That(deleted, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DeleteAsync_NonExistentId_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var nonExistentId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var result = await _service.DeleteAsync(nonExistentId);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DeleteAsync_ModelWithRobots_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
var model = TestHelpers.CreateTestRobotModel();
|
||||
_context.RobotModels.Add(model);
|
||||
|
||||
var robot = TestHelpers.CreateTestRobot(modelId: model.Id);
|
||||
_context.Robots.Add(robot);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.ThrowsAsync<InvalidOperationException>(async () => await _service.DeleteAsync(model.Id));
|
||||
Assert.That(ex, Is.Not.Null);
|
||||
Assert.That(ex!.Message, Does.Contain("Cannot delete").Or.Contain("cannot be deleted").Or.Contain("being used"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SearchAsync_WithQuery_ReturnsMatchingModels()
|
||||
{
|
||||
// Arrange
|
||||
TestHelpers.SeedTestData(_context);
|
||||
|
||||
// Act
|
||||
var result = await _service.SearchAsync("AMR-T800");
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result.Count, Is.EqualTo(1));
|
||||
Assert.That(result[0].ModelName, Is.EqualTo("AMR-T800"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SearchAsync_EmptyQuery_ReturnsAllModels()
|
||||
{
|
||||
// Arrange
|
||||
TestHelpers.SeedTestData(_context);
|
||||
|
||||
// Act
|
||||
var result = await _service.SearchAsync("");
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result.Count, Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ExistsAsync_ExistingModelName_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var model = TestHelpers.CreateTestRobotModel(modelName: "ExistingModel");
|
||||
_context.RobotModels.Add(model);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await _service.ExistsAsync("ExistingModel");
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ExistsAsync_NonExistentModelName_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
// No models in database
|
||||
|
||||
// Act
|
||||
var result = await _service.ExistsAsync("NonExistentModel");
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetUsageInfoAsync_ModelWithRobots_ReturnsUsageInfo()
|
||||
{
|
||||
// Arrange
|
||||
var model = TestHelpers.CreateTestRobotModel();
|
||||
_context.RobotModels.Add(model);
|
||||
|
||||
var robot1 = TestHelpers.CreateTestRobot(modelId: model.Id);
|
||||
var robot2 = TestHelpers.CreateTestRobot(robotId: "ROBOT-002", modelId: model.Id);
|
||||
_context.Robots.AddRange(robot1, robot2);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await _service.GetUsageInfoAsync(model.Id);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result.RobotCount, Is.EqualTo(2));
|
||||
Assert.That(result.CanDelete, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetUsageInfoAsync_ModelWithoutRobots_ReturnsZeroCount()
|
||||
{
|
||||
// Arrange
|
||||
var model = TestHelpers.CreateTestRobotModel();
|
||||
_context.RobotModels.Add(model);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await _service.GetUsageInfoAsync(model.Id);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result.RobotCount, Is.EqualTo(0));
|
||||
Assert.That(result.CanDelete, Is.True);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using RobotNet10.FleetManager.Data;
|
||||
using RobotNet10.FleetManager.Services;
|
||||
using RobotNet10.FleetManager.Shared.DTOs.Robot;
|
||||
using RobotNet10.RobotManager.Test.Helpers;
|
||||
|
||||
namespace RobotNet10.RobotManager.Test.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for RobotService
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class RobotServiceTests
|
||||
{
|
||||
private ApplicationDbContext _context = null!;
|
||||
private RobotService _service = null!;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
|
||||
_context = new ApplicationDbContext(options);
|
||||
var logger = new LoggerFactory().CreateLogger<RobotService>();
|
||||
var _logger = new FleetManager.Services.Logger<RobotService>(logger);
|
||||
_service = new RobotService(_context, _logger);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
_context.Dispose();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CreateAsync_ValidData_ReturnsCreatedRobot()
|
||||
{
|
||||
// Arrange
|
||||
var model = TestHelpers.CreateTestRobotModel();
|
||||
_context.RobotModels.Add(model);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var request = new CreateRobotRequest
|
||||
{
|
||||
RobotId = "ROBOT-001",
|
||||
Name = "Test Robot",
|
||||
ModelId = model.Id,
|
||||
MapId = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _service.CreateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result.RobotId, Is.EqualTo(request.RobotId));
|
||||
Assert.That(result.Name, Is.EqualTo(request.Name));
|
||||
Assert.That(result.ModelId, Is.EqualTo(request.ModelId));
|
||||
Assert.That(result.Id, Is.Not.EqualTo(Guid.Empty));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CreateAsync_DuplicateRobotId_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
var model = TestHelpers.CreateTestRobotModel();
|
||||
_context.RobotModels.Add(model);
|
||||
|
||||
var existingRobot = TestHelpers.CreateTestRobot(robotId: "ROBOT-001", modelId: model.Id);
|
||||
_context.Robots.Add(existingRobot);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var request = new CreateRobotRequest
|
||||
{
|
||||
RobotId = "ROBOT-001",
|
||||
Name = "Another Robot",
|
||||
ModelId = model.Id
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.ThrowsAsync<InvalidOperationException>(async () => await _service.CreateAsync(request));
|
||||
Assert.That(ex!.Message, Does.Contain("already exists"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CreateAsync_NonExistentModelId_ThrowsKeyNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
var request = new CreateRobotRequest
|
||||
{
|
||||
RobotId = "ROBOT-001",
|
||||
Name = "Test Robot",
|
||||
ModelId = Guid.NewGuid() // Non-existent model
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
Assert.ThrowsAsync<KeyNotFoundException>(async () => await _service.CreateAsync(request));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetAllAsync_ReturnsAllRobots()
|
||||
{
|
||||
// Arrange
|
||||
TestHelpers.SeedTestData(_context);
|
||||
|
||||
// Act
|
||||
var result = await _service.GetAllAsync();
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result.Count, Is.EqualTo(3));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetAllAsync_WithModelIdFilter_ReturnsFilteredRobots()
|
||||
{
|
||||
// Arrange
|
||||
TestHelpers.SeedTestData(_context);
|
||||
var modelId = Guid.Parse("11111111-1111-1111-1111-111111111111");
|
||||
|
||||
// Act
|
||||
var result = await _service.GetAllAsync(modelId, null);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result.Count, Is.EqualTo(2)); // 2 robots use model1
|
||||
Assert.That(result.All(r => r.ModelId == modelId), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetByIdAsync_ExistingId_ReturnsRobot()
|
||||
{
|
||||
// Arrange
|
||||
var model = TestHelpers.CreateTestRobotModel();
|
||||
_context.RobotModels.Add(model);
|
||||
|
||||
var robot = TestHelpers.CreateTestRobot(modelId: model.Id);
|
||||
_context.Robots.Add(robot);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await _service.GetByIdAsync(robot.Id);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result!.Id, Is.EqualTo(robot.Id));
|
||||
Assert.That(result.RobotId, Is.EqualTo(robot.RobotId));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetByIdAsync_NonExistentId_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var nonExistentId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var result = await _service.GetByIdAsync(nonExistentId);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetByRobotIdAsync_ExistingRobotId_ReturnsRobot()
|
||||
{
|
||||
// Arrange
|
||||
var model = TestHelpers.CreateTestRobotModel();
|
||||
_context.RobotModels.Add(model);
|
||||
|
||||
var robot = TestHelpers.CreateTestRobot(robotId: "ROBOT-001", modelId: model.Id);
|
||||
_context.Robots.Add(robot);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await _service.GetByRobotIdAsync("ROBOT-001");
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result!.RobotId, Is.EqualTo("ROBOT-001"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetByRobotIdAsync_NonExistentRobotId_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
// No robots in database
|
||||
|
||||
// Act
|
||||
var result = await _service.GetByRobotIdAsync("NON-EXISTENT");
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task UpdateAsync_ValidData_UpdatesRobot()
|
||||
{
|
||||
// Arrange
|
||||
var model = TestHelpers.CreateTestRobotModel();
|
||||
_context.RobotModels.Add(model);
|
||||
|
||||
var robot = TestHelpers.CreateTestRobot(modelId: model.Id);
|
||||
_context.Robots.Add(robot);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var request = new UpdateRobotRequest
|
||||
{
|
||||
RobotId = "UPDATED-001",
|
||||
Name = "Updated Robot",
|
||||
ModelId = model.Id,
|
||||
MapId = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _service.UpdateAsync(robot.Id, request);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result.RobotId, Is.EqualTo(request.RobotId));
|
||||
Assert.That(result.Name, Is.EqualTo(request.Name));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task UpdateAsync_NonExistentId_ThrowsKeyNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
var nonExistentId = Guid.NewGuid();
|
||||
var request = new UpdateRobotRequest
|
||||
{
|
||||
RobotId = "ROBOT-001",
|
||||
Name = "Test",
|
||||
ModelId = Guid.NewGuid()
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
Assert.ThrowsAsync<KeyNotFoundException>(async () => await _service.UpdateAsync(nonExistentId, request));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DeleteAsync_ExistingId_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var model = TestHelpers.CreateTestRobotModel();
|
||||
_context.RobotModels.Add(model);
|
||||
|
||||
var robot = TestHelpers.CreateTestRobot(modelId: model.Id);
|
||||
_context.Robots.Add(robot);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await _service.DeleteAsync(robot.Id);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.True);
|
||||
var deleted = await _context.Robots.FindAsync(robot.Id);
|
||||
Assert.That(deleted, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DeleteAsync_NonExistentId_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var nonExistentId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var result = await _service.DeleteAsync(nonExistentId);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetByModelIdAsync_ReturnsRobotsForModel()
|
||||
{
|
||||
// Arrange
|
||||
TestHelpers.SeedTestData(_context);
|
||||
var modelId = Guid.Parse("11111111-1111-1111-1111-111111111111");
|
||||
|
||||
// Act
|
||||
var result = await _service.GetByModelIdAsync(modelId);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result.Count, Is.EqualTo(2));
|
||||
Assert.That(result.All(r => r.ModelId == modelId), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SearchAsync_WithQuery_ReturnsMatchingRobots()
|
||||
{
|
||||
// Arrange
|
||||
TestHelpers.SeedTestData(_context);
|
||||
|
||||
// Act
|
||||
var result = await _service.SearchAsync("ROBOT-001");
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result.Count, Is.EqualTo(1));
|
||||
Assert.That(result[0].RobotId, Is.EqualTo("ROBOT-001"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SearchAsync_EmptyQuery_ReturnsAllRobots()
|
||||
{
|
||||
// Arrange
|
||||
TestHelpers.SeedTestData(_context);
|
||||
|
||||
// Act
|
||||
var result = await _service.SearchAsync("");
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result.Count, Is.EqualTo(3));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ExistsAsync_ExistingRobotId_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var model = TestHelpers.CreateTestRobotModel();
|
||||
_context.RobotModels.Add(model);
|
||||
|
||||
var robot = TestHelpers.CreateTestRobot(robotId: "ROBOT-001", modelId: model.Id);
|
||||
_context.Robots.Add(robot);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await _service.ExistsAsync("ROBOT-001");
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ExistsAsync_NonExistentRobotId_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
// No robots in database
|
||||
|
||||
// Act
|
||||
var result = await _service.ExistsAsync("NON-EXISTENT");
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.False);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user