Making Unit Testing Easy with FakeXrmEasy
Hey there! Let's talk about making your Dataverse testing life easier with FakeXrmEasy. If you've been wrestling with unit tests for your Dynamics 365/Dataverse projects, you're in for a treat.
Why You'll Love FakeXrmEasy
Think of FakeXrmEasy as your personal Dataverse simulator. Instead of mocking every single service call (we've all been there, it's not fun), FakeXrmEasy gives you an in-memory database that just works. No more endless mocking – just write your tests and go!
Key Features
- Complete simulation of Dataverse backend with in-memory database
- Support for all query types:
- FetchXML
- QueryExpress
- QueryByAttribute
- CRM LINQ provider
- Compatible with major test runners (xUnit, MSTest, NUnit)
Real-World Testing Example
Let's look at a practical example. Say you're updating contact records:
public class ContactTests : FakeXrmEasyTestsBase { private readonly Contact _contact; public ContactTests() { // Create a test contact - just like you would in real life _contact = new Contact { Id = Guid.NewGuid(), FirstName = "John", LastName = "Smith", EmailAddress1 = "john.smith@email.com" }; } [Fact] public async Task Can_Update_Contact_Name_And_Email() { // Start with our test contact context.Initialize(_contact); // Let's update John's details var command = new UpdateContactCommand(service) { ContactId = _contact.Id, FirstName = "Jonathan", EmailAddress1 = "jonathan.smith@email.com" }; await command.ExecuteAsync(); // Did it work? Let's check! var updatedContact = context.CreateQuery<Contact>() .Where(c => c.Id == _contact.Id) .FirstOrDefault(); Assert.Equal("Jonathan", updatedContact.FirstName); Assert.Equal("jonathan.smith@email.com", updatedContact.EmailAddress1); } }
Here's the command that makes it happen:
public class UpdateContactCommand { private readonly IOrganizationService _service; public Guid ContactId { get; set; } public string FirstName { get; set; } public string EmailAddress1 { get; set; } public UpdateContactCommand(IOrganizationService service) { _service = service ?? throw new ArgumentNullException(nameof(service)); } public async Task ExecuteAsync() { var contact = new Contact { Id = ContactId, FirstName = FirstName, EmailAddress1 = EmailAddress1 }; await _service.UpdateAsync(contact); } }
Quick Tips for Better Tests
- Keep it simple - test one thing at a time
- Only set up the data you need
- Give your tests clear, descriptive names
- Check the results right after your operation
Testing Queries Made Easy
Here's how you might test a query to find active contacts:
[Fact] public void Find_Active_Contacts() { // Set up some test contacts var contacts = new List<Contact> { new Contact { Id = Guid.NewGuid(), FirstName = "Active", StateCode = ContactState.Active }, new Contact { Id = Guid.NewGuid(), FirstName = "Inactive", StateCode = ContactState.Inactive } }; context.Initialize(contacts); // Find active contacts var activeContacts = context.CreateQuery<Contact>() .Where(c => c.StateCode == ContactState.Active) .ToList(); // Make sure we found the right one Assert.Single(activeContacts); Assert.Equal("Active", activeContacts[0].FirstName); }
Basic CRUD Testing
[Fact] public async Task Create_And_Update_Account_With_Related_Contacts() { // Setup test data var account = new Account { Id = Guid.NewGuid(), Name = "Test Corp" }; var contact = new Contact { Id = Guid.NewGuid(), FirstName = "John", LastName = "Doe", ParentCustomerId = account.ToEntityReference() }; context.Initialize(new Entity[] { account, contact }); // Test update logic var updateCmd = new UpdateAccountCommand(service) { AccountId = account.Id, NewName = "Test Corp International", Revenue = new Money(1000000) }; await updateCmd.ExecuteAsync(); // Verify results var updatedAccount = context.CreateQuery<Account>() .Include(a => a.Contact_Customer) // Related contacts .FirstOrDefault(a => a.Id == account.Id); Assert.Equal("Test Corp International", updatedAccount.Name); Assert.Equal(1000000, updatedAccount.Revenue.Value); }
Plugin Testing
public class AccountPostCreatePlugin : IPlugin { public void Execute(IServiceProvider serviceProvider) { var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext)); var service = ((IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory))) .CreateOrganizationService(context.UserId); var account = context.InputParameters["Target"] as Entity; // Create associated task var task = new Task { Subject = $"Review new account: {account.GetAttributeValue<string>("name")}", RegardingObjectId = account.ToEntityReference() }; service.Create(task); } } public void Plugin_Creates_Task_On_Account_Creation() { // Setup plugin context var plugin = new AccountPostCreatePlugin(); var account = new Account { Id = Guid.NewGuid(), Name = "New Account" }; // Setup execution context var pluginContext = context.GetDefaultPluginContext(); pluginContext.InputParameters = new ParameterCollection { { "Target", account } }; pluginContext.Stage = 40; // Post-operation // Execute plugin plugin.Execute(context.GetDefaultServiceProvider(pluginContext)); // Verify task creation var createdTask = context.CreateQuery<Task>() .FirstOrDefault(t => t.RegardingObjectId.Id == account.Id); Assert.NotNull(createdTask); Assert.Contains("Review new account", createdTask.Subject); }
Custom Action Testing
public class CloseOpportunityRequest : OrganizationRequest { public CloseOpportunityRequest() : base("new_CloseOpportunity") { } public Guid OpportunityId { get { return (Guid)Parameters["OpportunityId"]; } set { Parameters["OpportunityId"] = value; } } public int Status { get { return (int)Parameters["Status"]; } set { Parameters["Status"] = value; } } } [Fact] public void Test_Close_Opportunity_Action() { // Setup var opportunity = new Opportunity { Id = Guid.NewGuid(), Name = "Test Deal", StatusCode = new OptionSetValue(1) }; context.Initialize(opportunity); // Register custom action context.AddExecutionMock("new_CloseOpportunity", (req) => { var opportunityId = (Guid)req.Parameters["OpportunityId"]; var status = (int)req.Parameters["Status"]; // Update opportunity var opp = new Opportunity { Id = opportunityId, StateCode = new OptionSetValue(1), StatusCode = new OptionSetValue(status) }; context.GetOrganizationService().Update(opp); return new OrganizationResponse(); }); // Execute action var request = new CloseOpportunityRequest { OpportunityId = opportunity.Id, Status = 2 // Won }; context.GetOrganizationService().Execute(request); // Verify var updatedOpp = context.CreateQuery<Opportunity>() .FirstOrDefault(o => o.Id == opportunity.Id); Assert.Equal(2, updatedOpp.StatusCode.Value); }
Workflow Testing
public class CreateFollowUpTaskActivity : CodeActivity { [Input("Account")] [ReferenceTarget("account")] public InArgument<EntityReference> Account { get; set; } protected override void Execute(CodeActivityContext executionContext) { var workflowContext = executionContext.GetExtension<IWorkflowContext>(); var serviceFactory = executionContext.GetExtension<IOrganizationServiceFactory>(); var service = serviceFactory.CreateOrganizationService(workflowContext.UserId); var task = new Task { Subject = "Follow up with account", RegardingObjectId = Account.Get(executionContext) }; service.Create(task); } } [Fact] public void Workflow_Creates_Follow_Up_Task() { // Setup var account = new Account { Id = Guid.NewGuid(), Name = "Test Account" }; context.Initialize(account); // Setup workflow var workflow = new CreateFollowUpTaskActivity(); var workflowInvoker = new WorkflowInvoker(workflow); // Execute workflow var inputs = new Dictionary<string, object> { { "Account", account.ToEntityReference() } }; workflowInvoker.Invoke(inputs); // Verify var createdTask = context.CreateQuery<Task>() .FirstOrDefault(t => t.RegardingObjectId.Id == account.Id); Assert.NotNull(createdTask); Assert.Equal("Follow up with account", createdTask.Subject); }
Testing Complex Scenarios
Business Rules Testing
[Fact] public void Contact_Business_Rules_Test() { // Setup business rules context.EnableBusinessRules(); var contact = new Contact { Id = Guid.NewGuid(), EmailAddress1 = "invalid-email" }; // Should throw due to business rule violation Assert.Throws<InvalidPluginExecutionException>(() => context.GetOrganizationService().Create(contact)); }
Multi-Entity Operations
[Fact] public async Task Create_Account_With_Primary_Contact() { var data = new AccountWithContactCommand(service) { AccountName = "Enterprise Corp", ContactFirstName = "Jane", ContactLastName = "Smith", ContactEmail = "jane.smith@enterprise.com" }; var result = await data.ExecuteAsync(); // Verify account creation var account = context.CreateQuery<Account>() .FirstOrDefault(a => a.Id == result.AccountId); Assert.NotNull(account); // Verify contact creation and association var contact = context.CreateQuery<Contact>() .FirstOrDefault(c => c.Id == result.ContactId); Assert.NotNull(contact); Assert.Equal(account.Id, contact.ParentCustomerId.Id); }
Best Practices Summary
- Initialize minimal test data
- Test business logic isolation
- Use meaningful test names
- One assertion concept per test
- Mock external services when needed
- Handle async operations properly
- Test error scenarios
Common Pitfalls
- Not clearing context between tests
- Testing too many scenarios in one test
- Not handling plugin context properly
- Forgetting to register custom actions
- Not testing negative scenarios
Remember, FakeXrmEasy handles all the heavy lifting of simulating Dataverse. You just focus on writing clean, meaningful tests. No more mock setup headaches!
No comments:
Post a Comment