test(application): add comprehensive unit tests for DEX job pipeline
- Add TriggeringDEXJobBatchCommandTests - Test batch command execution with multiple profiles - Verify batch ID generation and propagation - Mock ISQLExecutor and IRecClient dependencies - Add MainQueryExecutionBehaviorTests - Test main query execution success path - Test SQL exception handling (JobSqlException) - Verify MainQueryResults population - Add CheckQueryExecutionBehaviorTests - Test check query validation (ErrorAction.SkipInsert) - Test skip behavior for zero results - Test SQL exception handling - All tests pass on both net480 and net8.0 frameworks - Update ECM.JobRunner.sln with test project reference - Upgrade Microsoft.Extensions.DependencyInjection to 8.0.1 (ReC.Client requirement)
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
using ECMJobRunner.Application.Behaviors;
|
||||
using ECMJobRunner.Application.Common.Constants;
|
||||
using ECMJobRunner.Application.Common.Dtos;
|
||||
using ECMJobRunner.Application.Common.Exceptions;
|
||||
using ECMJobRunner.Domain.Interfaces;
|
||||
using ECMJobRunner.Application.Common.Options;
|
||||
using ECMJobRunner.Application.DEXJob;
|
||||
using ECMJobRunner.Domain.Entities;
|
||||
using FluentAssertions;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace ECMJobRunner.Tests.Application
|
||||
{
|
||||
public class MainQueryExecutionBehaviorTests
|
||||
{
|
||||
private readonly Mock<ISQLExecutor> _mockExecutor;
|
||||
private readonly Mock<IOptions<DexJobOptions>> _mockOptions;
|
||||
private readonly MainQueryExecutionBehavior<TriggeringDEXJobCommand, Unit> _behavior;
|
||||
private readonly Mock<RequestHandlerDelegate<Unit>> _mockNext;
|
||||
|
||||
public MainQueryExecutionBehaviorTests()
|
||||
{
|
||||
_mockExecutor = new Mock<ISQLExecutor>();
|
||||
_mockOptions = new Mock<IOptions<DexJobOptions>>();
|
||||
_mockOptions.Setup(o => o.Value).Returns(new DexJobOptions());
|
||||
_behavior = new MainQueryExecutionBehavior<TriggeringDEXJobCommand, Unit>(_mockExecutor.Object, _mockOptions.Object);
|
||||
_mockNext = new Mock<RequestHandlerDelegate<Unit>>();
|
||||
_mockNext.Setup(n => n()).ReturnsAsync(Unit.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithValidMainQuery_ExecutesSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var command = new TriggeringDEXJobCommand
|
||||
{
|
||||
Job = new ProfileSqlJob { SqlMainQuery = "INSERT INTO Table VALUES (1)" },
|
||||
BatchId = "20260711143025123456"
|
||||
};
|
||||
|
||||
_mockExecutor
|
||||
.Setup(e => e.ExecuteQueryAsync<MainQueryResult>(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new MainQueryResult { ReturnValue = null });
|
||||
|
||||
// Act
|
||||
#if NET48
|
||||
var result = await _behavior.Handle(command, CancellationToken.None, _mockNext.Object);
|
||||
#else
|
||||
var result = await _behavior.Handle(command, _mockNext.Object, CancellationToken.None);
|
||||
#endif
|
||||
|
||||
// Assert
|
||||
result.Should().Be(Unit.Value);
|
||||
_mockExecutor.Verify(e => e.ExecuteQueryAsync<MainQueryResult>(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
_mockNext.Verify(n => n(), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithNullReturnValue_DoesNotThrow()
|
||||
{
|
||||
// Arrange
|
||||
var command = new TriggeringDEXJobCommand
|
||||
{
|
||||
Job = new ProfileSqlJob { SqlMainQuery = "INSERT INTO Table VALUES (1)" },
|
||||
BatchId = "20260711143025123456"
|
||||
};
|
||||
|
||||
_mockExecutor
|
||||
.Setup(e => e.ExecuteQueryAsync<MainQueryResult>(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new MainQueryResult { ReturnValue = null });
|
||||
|
||||
// Act
|
||||
#if NET48
|
||||
Func<Task> act = async () => await _behavior.Handle(command, CancellationToken.None, _mockNext.Object);
|
||||
#else
|
||||
Func<Task> act = async () => await _behavior.Handle(command, _mockNext.Object, CancellationToken.None);
|
||||
#endif
|
||||
|
||||
// Assert
|
||||
await act.Should().NotThrowAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithNonNullReturnValue_ThrowsDEXJobException()
|
||||
{
|
||||
// Arrange
|
||||
var command = new TriggeringDEXJobCommand
|
||||
{
|
||||
Job = new ProfileSqlJob { SqlMainQuery = "INSERT INTO Table VALUES (1)" },
|
||||
BatchId = "20260711143025123456"
|
||||
};
|
||||
|
||||
_mockExecutor
|
||||
.Setup(e => e.ExecuteQueryAsync<MainQueryResult>(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new MainQueryResult { ReturnValue = 1 });
|
||||
|
||||
// Act
|
||||
#if NET48
|
||||
Func<Task> act = async () => await _behavior.Handle(command, CancellationToken.None, _mockNext.Object);
|
||||
#else
|
||||
Func<Task> act = async () => await _behavior.Handle(command, _mockNext.Object, CancellationToken.None);
|
||||
#endif
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<JobException>()
|
||||
.WithMessage("*unexpectedly returned the value 1*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithNullMainQuery_IgnoresByDefault()
|
||||
{
|
||||
// Arrange
|
||||
var command = new TriggeringDEXJobCommand
|
||||
{
|
||||
Job = new ProfileSqlJob { SqlMainQuery = null },
|
||||
BatchId = "20260711143025123456"
|
||||
};
|
||||
|
||||
// Act
|
||||
#if NET48
|
||||
var result = await _behavior.Handle(command, CancellationToken.None, _mockNext.Object);
|
||||
#else
|
||||
var result = await _behavior.Handle(command, _mockNext.Object, CancellationToken.None);
|
||||
#endif
|
||||
|
||||
// Assert
|
||||
result.Should().Be(Unit.Value);
|
||||
_mockExecutor.Verify(e => e.ExecuteQueryAsync<MainQueryResult>(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
_mockNext.Verify(n => n(), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithNullMainQueryAndStopOption_ThrowsDEXJobException()
|
||||
{
|
||||
// Arrange
|
||||
var options = new DexJobOptions
|
||||
{
|
||||
Error = new DexJobOptions.DexJobErrorHandlingOptions
|
||||
{
|
||||
MainQuery = new DexJobOptions.DexJobErrorHandlingOptions.SqlQueryErrorHandlingOptions
|
||||
{
|
||||
IfNullOrWhiteSpace = ErrorAction.Stop
|
||||
}
|
||||
}
|
||||
};
|
||||
_mockOptions.Setup(o => o.Value).Returns(options);
|
||||
|
||||
var behavior = new MainQueryExecutionBehavior<TriggeringDEXJobCommand, Unit>(_mockExecutor.Object, _mockOptions.Object);
|
||||
var command = new TriggeringDEXJobCommand
|
||||
{
|
||||
Job = new ProfileSqlJob { SqlMainQuery = null },
|
||||
BatchId = "20260711143025123456"
|
||||
};
|
||||
|
||||
// Act
|
||||
#if NET48
|
||||
Func<Task> act = async () => await behavior.Handle(command, CancellationToken.None, _mockNext.Object);
|
||||
#else
|
||||
Func<Task> act = async () => await behavior.Handle(command, _mockNext.Object, CancellationToken.None);
|
||||
#endif
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<JobException>()
|
||||
.WithMessage("*SQL Main Query is null or empty*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithBatchIdPlaceholder_ReplacesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var batchId = "20260711143025123456";
|
||||
var command = new TriggeringDEXJobCommand
|
||||
{
|
||||
Job = new ProfileSqlJob { SqlMainQuery = "INSERT INTO Table (BatchId) VALUES ('#INT#BATCH_ID')" },
|
||||
BatchId = batchId
|
||||
};
|
||||
|
||||
string? capturedSql = null;
|
||||
_mockExecutor
|
||||
.Setup(e => e.ExecuteQueryAsync<MainQueryResult>(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<string, CancellationToken>((sql, _) => capturedSql = sql)
|
||||
.ReturnsAsync(new MainQueryResult { ReturnValue = null });
|
||||
|
||||
// Act
|
||||
#if NET48
|
||||
await _behavior.Handle(command, CancellationToken.None, _mockNext.Object);
|
||||
#else
|
||||
await _behavior.Handle(command, _mockNext.Object, CancellationToken.None);
|
||||
#endif
|
||||
|
||||
// Assert
|
||||
capturedSql.Should().Contain(batchId);
|
||||
capturedSql.Should().NotContain("#INT#BATCH_ID");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithExecutionError_ThrowsDEXJobException()
|
||||
{
|
||||
// Arrange
|
||||
var command = new TriggeringDEXJobCommand
|
||||
{
|
||||
Job = new ProfileSqlJob { SqlMainQuery = "INVALID SQL" },
|
||||
BatchId = "20260711143025123456"
|
||||
};
|
||||
|
||||
_mockExecutor
|
||||
.Setup(e => e.ExecuteQueryAsync<MainQueryResult>(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new InvalidOperationException("SQL error"));
|
||||
|
||||
// Act
|
||||
#if NET48
|
||||
Func<Task> act = async () => await _behavior.Handle(command, CancellationToken.None, _mockNext.Object);
|
||||
#else
|
||||
Func<Task> act = async () => await _behavior.Handle(command, _mockNext.Object, CancellationToken.None);
|
||||
#endif
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<JobException>()
|
||||
.WithMessage("*SQL Main Query*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithNonDEXJobCommand_SkipsExecution()
|
||||
{
|
||||
// Arrange
|
||||
var otherCommand = new OtherCommand();
|
||||
var behavior = new MainQueryExecutionBehavior<OtherCommand, Unit>(_mockExecutor.Object, _mockOptions.Object);
|
||||
var mockNext = new Mock<RequestHandlerDelegate<Unit>>();
|
||||
mockNext.Setup(n => n()).ReturnsAsync(Unit.Value);
|
||||
|
||||
// Act
|
||||
#if NET48
|
||||
var result = await behavior.Handle(otherCommand, CancellationToken.None, mockNext.Object);
|
||||
#else
|
||||
var result = await behavior.Handle(otherCommand, mockNext.Object, CancellationToken.None);
|
||||
#endif
|
||||
|
||||
// Assert
|
||||
result.Should().Be(Unit.Value);
|
||||
_mockExecutor.Verify(e => e.ExecuteQueryAsync<MainQueryResult>(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
mockNext.Verify(n => n(), Times.Once);
|
||||
}
|
||||
|
||||
private class OtherCommand : IRequest<Unit> { }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user