| name | pacx-unit-test-writer |
| description | Guide for writing local unit tests for PACX commands without relying on external services. Use this skill when asked to write, generate, or add unit tests for a PACX command class or executor. Tests produced by this skill are fully local — they never connect to Dataverse or any other external service.
|
PACX Unit Test Writer
All tests live in Greg.Xrm.Command.Core.TestSuite\Commands\<Domain>\ and mirror the source layout under Greg.Xrm.Command.Core\Commands\<Domain>\.
Every command needs two test files:
| File | Tests |
|---|
<Verb>CommandTest.cs | CLI argument parsing — no DI, no Dataverse |
<Verb>CommandExecutorTest.cs | Executor logic — all dependencies mocked |
GlobalUsings.cs already imports Microsoft.VisualStudio.TestTools.UnitTesting, Greg.Xrm.Command.Services.Output, and Moq. Do not add redundant using statements for those.
Key test infrastructure
Utility.TestParseCommand<T>(params string[] args)
Parses a CLI argument array into a typed command instance. Use this for all parser tests.
var command = Utility.TestParseCommand<DeleteCommand>("table", "delete", "--name", "my_table");
OutputToMemory
A concrete, in-memory IOutput implementation. Captures everything written to it.
- Use when you need to assert on output text (e.g., verify an error message was printed).
output.ToString() returns the full captured text.
- Color-tagged output is serialised as
<Green>Done</Green> — you can search for substrings.
var output = new OutputToMemory();
StringAssert.Contains(output.ToString(), "Done");
Mock<IOutput>
Use instead of OutputToMemory when you need to verify specific IOutput calls with Moq's Verify.
var mockOutput = new Mock<IOutput>();
mockOutput.Setup(o => o.Write(It.IsAny<object>())).Returns(mockOutput.Object);
mockOutput.Setup(o => o.WriteLine(It.IsAny<object>(), It.IsAny<ConsoleColor>())).Returns(mockOutput.Object);
mockOutput.Verify(o => o.WriteLine(
It.Is<string>(s => s.Contains("Table not found")),
It.IsAny<ConsoleColor>()), Times.Once);
Rule of thumb: prefer OutputToMemory for most tests; switch to Mock<IOutput> only when you need to assert that a specific message was (or was not) written.
File 1 — <Verb>CommandTest.cs (parser tests)
Test every [Option] with both its long name and its short name. Also assert that default values and null optionals are set correctly when options are omitted.
namespace Greg.Xrm.Command.Commands.<Domain>
{
[TestClass]
public class <Verb>CommandTest
{
[TestMethod]
public void ParseWithLongNameShouldWork()
{
var command = Utility.TestParseCommand<<Verb>Command>(
"<noun>", "<verb>",
"--name", "TestValue");
Assert.AreEqual("TestValue", command.Name);
}
[TestMethod]
public void ParseWithShortNameShouldWork()
{
var command = Utility.TestParseCommand<<Verb>Command>(
"<noun>", "<verb>",
"-n", "TestValue");
Assert.AreEqual("TestValue", command.Name);
}
[TestMethod]
public void DefaultValuesShouldBeSetCorrectly()
{
var command = Utility.TestParseCommand<<Verb>Command>(
"<noun>", "<verb>",
"--name", "TestValue");
Assert.AreEqual(ExpectedDefaultForSomeOption, command.SomeOption);
Assert.IsNull(command.OptionalOption);
Assert.IsNull(command.SolutionName);
}
[TestMethod]
public void EnumOptionShouldBeParsedCorrectly()
{
var command = Utility.TestParseCommand<<Verb>Command>(
"<noun>", "<verb>",
"--name", "TestValue",
"--ownership", "Organization");
Assert.AreEqual(OwnershipTypes.OrgOwned, command.Ownership);
}
[TestMethod]
public void BoolFlagShouldDefaultToFalse()
{
var command = Utility.TestParseCommand<<Verb>Command>("<noun>", "<verb>", "--name", "v");
Assert.IsFalse(command.SomeFlag);
}
[TestMethod]
public void BoolFlagShouldBeParsedWhenProvided()
{
var command = Utility.TestParseCommand<<Verb>Command>(
"<noun>", "<verb>",
"--name", "v",
"--someFlag");
Assert.IsTrue(command.SomeFlag);
}
[TestMethod]
[DataRow("--name", "v1")]
[DataRow("--otherName", "v2")]
public void AllNameAliasesShouldMapToSameProperty(string optionKey, string optionValue)
{
var command = Utility.TestParseCommand<<Verb>Command>("<noun>", "<verb>", optionKey, optionValue);
Assert.IsNotNull(command.Name);
}
}
}
File 2 — <Verb>CommandExecutorTest.cs (executor unit tests)
Boilerplate — standard Dataverse mock setup
using Greg.Xrm.Command.Services.Connection;
using Microsoft.PowerPlatform.Dataverse.Client;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
namespace Greg.Xrm.Command.Commands.<Domain>
{
[TestClass]
public class <Verb>CommandExecutorTest
{
private static (
OutputToMemory output,
Mock<IOrganizationServiceRepository> repoMock,
Mock<IOrganizationServiceAsync2> crmMock)
CreateMocks()
{
var output = new OutputToMemory();
var crmMock = new Mock<IOrganizationServiceAsync2>();
var repoMock = new Mock<IOrganizationServiceRepository>();
repoMock
.Setup(r => r.GetCurrentConnectionAsync())
.ReturnsAsync(crmMock.Object);
return (output, repoMock, crmMock);
}
}
}
Happy-path test
[TestMethod]
public async Task ExecuteAsync_ShouldSucceed_WhenInputIsValid()
{
var (output, repoMock, crmMock) = CreateMocks();
OrganizationRequest? capturedRequest = null;
crmMock
.Setup(c => c.ExecuteAsync(It.IsAny<OrganizationRequest>()))
.Callback<OrganizationRequest>(r => capturedRequest = r)
.ReturnsAsync(new <ExpectedResponseType>());
var executor = new <Verb>CommandExecutor(output, repoMock.Object);
var result = await executor.ExecuteAsync(
new <Verb>Command { Name = "some_value" },
CancellationToken.None);
Assert.IsTrue(result.IsSuccess, result.ErrorMessage);
repoMock.Verify(r => r.GetCurrentConnectionAsync(), Times.Once);
crmMock.Verify(c => c.ExecuteAsync(It.IsAny<<ExpectedRequestType>>()), Times.Once);
Assert.IsNotNull(capturedRequest);
var typedRequest = capturedRequest as <ExpectedRequestType>;
Assert.IsNotNull(typedRequest);
Assert.AreEqual("some_value", typedRequest.);
}
Failure — Dataverse returns an error
[TestMethod]
public async Task ExecuteAsync_ShouldFail_WhenDataverseThrows()
{
var (output, repoMock, crmMock) = CreateMocks();
crmMock
.Setup(c => c.ExecuteAsync(It.IsAny<OrganizationRequest>()))
.ThrowsAsync(new System.ServiceModel.FaultException<Microsoft.Xrm.Sdk.OrganizationServiceFault>(
new Microsoft.Xrm.Sdk.OrganizationServiceFault(),
"Simulated Dataverse fault"));
var executor = new <Verb>CommandExecutor(output, repoMock.Object);
var result = await executor.ExecuteAsync(
new <Verb>Command { Name = "some_value" },
CancellationToken.None);
Assert.IsFalse(result.IsSuccess);
Assert.IsFalse(string.IsNullOrWhiteSpace(result.ErrorMessage));
}
Failure — prerequisite check fails (e.g. solution not found, entity not found)
[TestMethod]
public async Task ExecuteAsync_ShouldFail_WhenSolutionDoesNotExist()
{
var (output, repoMock, crmMock) = CreateMocks();
crmMock
.Setup(c => c.RetrieveMultipleAsync(It.IsAny<Microsoft.Xrm.Sdk.Query.QueryBase>()))
.ReturnsAsync(new Microsoft.Xrm.Sdk.EntityCollection());
var executor = new <Verb>CommandExecutor(output, repoMock.Object);
var result = await executor.ExecuteAsync(
new <Verb>Command { Name = "v", SolutionName = "nonexistent" },
CancellationToken.None);
Assert.IsFalse(result.IsSuccess);
crmMock.Verify(c => c.ExecuteAsync(It.IsAny<OrganizationRequest>()), Times.Never);
}
Failure — no default solution set
[TestMethod]
public async Task ExecuteAsync_ShouldFail_WhenNoSolutionProvidedAndNoDefault()
{
var (output, repoMock, crmMock) = CreateMocks();
repoMock
.Setup(r => r.GetCurrentDefaultSolutionAsync())
.ReturnsAsync((string?)null);
var executor = new <Verb>CommandExecutor(output, repoMock.Object);
var result = await executor.ExecuteAsync(
new <Verb>Command { Name = "some_value" },
CancellationToken.None);
Assert.IsFalse(result.IsSuccess);
crmMock.Verify(c => c.ExecuteAsync(It.IsAny<OrganizationRequest>()), Times.Never);
}
Mocking additional injected services
When the executor depends on more services beyond IOrganizationServiceRepository, mock and inject them the same way:
var mockPluralization = new Mock<IPluralizationFactory>();
mockPluralization
.Setup(p => p.CreateFor(It.IsAny<int>()))
.Returns(Mock.Of<IPluralizationStrategy>(s =>
s.GetPluralForAsync(It.IsAny<string>()) == Task.FromResult("Values")));
var executor = new <Verb>CommandExecutor(output, repoMock.Object, mockPluralization.Object);
Parameterised executor tests with [DataRow]
Use [DataRow] when several input combinations should all produce the same outcome:
[TestMethod]
[DataRow("value_a", true)]
[DataRow("value_b", true)]
[DataRow(null, false)]
public async Task ExecuteAsync_ShouldReturnExpectedResult(string? inputName, bool expectSuccess)
{
var (output, repoMock, crmMock) = CreateMocks();
crmMock
.Setup(c => c.ExecuteAsync(It.IsAny<OrganizationRequest>()))
.ReturnsAsync(new OrganizationResponse());
var executor = new <Verb>CommandExecutor(output, repoMock.Object);
var result = await executor.ExecuteAsync(
new <Verb>Command { Name = inputName },
CancellationToken.None);
Assert.AreEqual(expectSuccess, result.IsSuccess);
}
Testing IValidatableObject.Validate() directly
When a command implements IValidatableObject, test Validate() in isolation — no parsing, no executor.
[TestMethod]
public void Validate_ShouldFail_WhenMutuallyExclusiveOptionsAreProvided()
{
var command = new <Verb>Command
{
OptionA = "value",
OptionB = "also-value"
};
var context = new System.ComponentModel.DataAnnotations.ValidationContext(command);
var results = command.Validate(context).ToList();
Assert.AreEqual(1, results.Count);
CollectionAssert.Contains(results[0].MemberNames.ToList(), nameof(<Verb>Command.OptionA));
}
[TestMethod]
public void Validate_ShouldPass_WhenOnlyOneOptionIsProvided()
{
var command = new <Verb>Command { OptionA = "value" };
var context = new System.ComponentModel.DataAnnotations.ValidationContext(command);
var results = command.Validate(context).ToList();
Assert.AreEqual(0, results.Count);
}
What NOT to do
| ❌ Avoid | ✅ Do instead |
|---|
new OrganizationServiceRepository(...) in a unit test | new Mock<IOrganizationServiceRepository>() |
[TestCategory("Integration")] on a local test | Omit the category entirely |
task.Wait() | await executor.ExecuteAsync(...) |
Asserting only Assert.IsNotNull(output.ToString()) | Assert on result.IsSuccess, mock .Verify(), and request content |
| Real file I/O or environment variables | Mock the service that reads them |
new OutputToConsole() | new OutputToMemory() or new Mock<IOutput>() |
Checklist before finishing