This example illustrates how we can use a Unit Test Project and Moq to simulate execution of a Dataverse plugin and develop/debug directly in Visual Studio against a real Organization Service without having to continuously deploy new versions of the plugin.
The aim here is to help speed up plugin development by giving the developer a way of quickly and easily examining how their plugin is behaving. Instead of mocking all the data that your plugin needs, this uses the actual organization service for an environment and will create/update/delete/read real Dataverse data, so is only appropriate for use against development environments.
The steps below go through the setup of a basic plugin project and unit test project that invokes the plugin.
- Create a directory to hold the Dataverse plugin project and testing project. Run the following command in a terminal:
cd C:\{YourReposPath}
mkdir PluginTestingForLazyPeople
cd C:\{YourReposPath}\PluginTestingForLazyPeople
- Create a new Dataverse plugin project using the PAC CLI. Run the following command in a terminal:
pac plugin init --author You --outputDirectory C:\{YourReposPath}\PluginTestingForLazyPeople\ExamplePluginTests
-
Open Visual Studio and select 'Open a project or solution'.
-
Select the ExamplePluginTests.csproj file that was created.
-
When Visual Studio opens, select the 'ExamplePluginTests' solution, then click File --> Save ExamplePluginTests.sln As.. and save the .sln file in the
PluginTestingForLazyPeople
directory.
-
Right click on the Solution and select Add --> New Project
-
Right click on the Unit Test Project that has been created and select Build Dependencies --> Project Dependencies
-
Right click on 'References' in the Unit Test Project and select Migrate packages.config to PackageReference...
-
Right click on References again and select Manage NuGet Packages...
We are going to create a Dataverse ServiceClient in the Unit Test Project and use it to pass a mock organization service into the plugin. This will then allow us to set breakpoints and debug plugin execution directly in Visual Studio without having to continuously deploy updated plugins to the server.
-
Right click on 'References' in the Unit Test Project and select Manage NuGet Packages...
-
Search for and install
Microsoft.PowerPlatform.Dataverse.Client
-
Within
TestMethod1()
we want to create aServiceClient
instance when the test runs. Update the fileUnitTest1.cs
as follows:using Microsoft.PowerPlatform.Dataverse.Client; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; namespace LazyPluginTests { [TestClass] public class UnitTest1 { [TestMethod] public void TestMethod1() { string connString = "AuthType=OAuth;Url=https://{{YOURORG}}.crm.dynamics.com/;Username={{YOURUSERNAME}};RedirectUri=http://localhost;AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;LoginPrompt=Auto"; ServiceClient serviceClient = new ServiceClient(connString); } } }
In our example, a login prompt will appear each time the test is run. See this article for using different connection strings: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/xrm-tooling/use-connection-strings-xrm-tooling-connect
-
Right click on the Unit Test Project and select Build
-
We are now going to attempt to run a test and observe that further setup is required. Expand the Test Explorer in Visual Studio, locate TestMethod1 and click Run
-
Observe that the test fails to run and returns an error about mismatched assembly manifests. This is a result of adding the Microsoft.PowerPlatform.Dataverse.Client assembly. To fix this, we will need to add binding redirects to the Unit Test Project
-
Right click on the Unit Test Project and select Add --> New Item
-
Update the
app.config
file as follows:<?xml version="1.0" encoding="utf-8" ?> <configuration> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="Microsoft.Extensions.DependencyInjection.Abstractions" publicKeyToken="adb9793829ddae60" culture="neutral"/> <bindingRedirect oldVersion="0.0.0.0-3.1.0.0" newVersion="3.1.8.0"/> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="Microsoft.Bcl.AsyncInterfaces" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/> <bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0"/> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="Microsoft.Extensions.Logging" publicKeyToken="adb9793829ddae60" culture="neutral"/> <bindingRedirect oldVersion="0.0.0.0-3.1.8.0" newVersion="3.1.8.0"/> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="Microsoft.Extensions.Options" publicKeyToken="adb9793829ddae60" culture="neutral"/> <bindingRedirect oldVersion="0.0.0.0-3.1.8.0" newVersion="3.1.8.0"/> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="Microsoft.Extensions.Logging.Abstractions" publicKeyToken="adb9793829ddae60" culture="neutral"/> <bindingRedirect oldVersion="0.0.0.0-3.1.8.0" newVersion="3.1.8.0"/> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="Microsoft.Extensions.Caching.Abstractions" publicKeyToken="adb9793829ddae60" culture="neutral"/> <bindingRedirect oldVersion="0.0.0.0-3.1.8.0" newVersion="3.1.8.0"/> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="Microsoft.Extensions.Primitives" publicKeyToken="adb9793829ddae60" culture="neutral"/> <bindingRedirect oldVersion="0.0.0.0-3.1.8.0" newVersion="3.1.8.0"/> </dependentAssembly> </assemblyBinding> </runtime> </configuration>
-
Right click on the Unit Test Project and select Rebuild
-
Attempt to run the TestMethod1 test again. This time we see that we are prompted for a login and the test should pass (note that nothing is actually being tested or asserted)
When a plugin executes on the platform an IServiceProvider
instance is provided to the Execute()
method of the plugin.
This gives us access to the services we use in a plugin (like the Organization Service) and is used to access the plugin execution context (such as the entity and event that triggered the plugin).
In TestMethod1()
we are going to create a Mock IServiceProvider
using Moq
, which will give us a way to simulate plugin execution by running a test and examine how a plugin behaves using a 'live' organization service.
Add the following code to TestMethod1()
:
var mockServiceProvider = new Moq.Mock<IServiceProvider>();
mockServiceProvider.Setup(x => x.GetService(typeof(IOrganizationService))).Returns(serviceClient);
/// Mocking the plugin execution context. In this example we are not providing any specific context such as message or target entity, but you can set it up as needed for your tests.
var mockExecutionContext = new Moq.Mock<IExecutionContext>();
mockExecutionContext.Setup(x => x.OperationCreatedOn).Returns(DateTime.Now);
mockExecutionContext.Setup(x => x.UserId).Returns(Guid.NewGuid());
mockExecutionContext.Setup(x => x.InitiatingUserId).Returns(Guid.NewGuid());
var mockPluginContext = new Moq.Mock<IPluginExecutionContext>();
mockPluginContext.Setup(x => x.UserId).Returns(Guid.NewGuid());
mockPluginContext.Setup(x => x.InitiatingUserId).Returns(Guid.NewGuid());
// This provides the 'real' service client we have created to the plugin
var mockServiceFactory = new Moq.Mock<IOrganizationServiceFactory>();
mockServiceFactory.Setup(x => x.CreateOrganizationService(It.IsAny<Guid>())).Returns(serviceClient);
var mockTracingService = new Moq.Mock<ITracingService>();
mockServiceProvider.Setup(x => x.GetService(typeof(ITracingService))).Returns(mockTracingService.Object);
mockServiceProvider.Setup(x => x.GetService(typeof(IOrganizationServiceFactory))).Returns(mockServiceFactory.Object);
mockServiceProvider.Setup(x => x.GetService(typeof(IPluginExecutionContext))).Returns(mockPluginContext.Object);
mockServiceProvider.Setup(x => x.GetService(typeof(IExecutionContext))).Returns(mockExecutionContext.Object);
- Update
UnitTest1.cs
to add a reference to the plugin project. Add the following using statement to the file:using DataversePlugin;
- Add the following code to
TestMethod1()
to invoke the Dataverse Plugin when the test is run:// Create an instance of the plugin and execute it with the mocked service provider. // The Plugin1 constructor requires unsecure and secure configuration strings, which can be null for testing purposes. var examplePlugin = new Plugin1(null, null); examplePlugin.Execute(mockServiceProvider.Object);
In this example I have updated Plugin1.cs
to perform some basic operations in order to demonstrate how we can observe and debug the plugin inside Visual Studio.
protected override void ExecuteDataversePlugin(ILocalPluginContext localPluginContext)
{
if (localPluginContext == null)
{
throw new ArgumentNullException(nameof(localPluginContext));
}
var context = localPluginContext.PluginExecutionContext;
// Create a record
Entity newContact = new Entity("contact");
newContact["firstname"] = $"Test Contact {DateTime.Now.Ticks}";
Guid newContactId = localPluginContext.PluginUserService.Create(newContact);
// Retrieve some records
QueryExpression getContacts = new QueryExpression("contact");
getContacts.TopCount = 5;
getContacts.ColumnSet = new ColumnSet("firstname");
EntityCollection contactsEC = localPluginContext.PluginUserService.RetrieveMultiple(getContacts);
var contactOne = contactsEC.Entities.FirstOrDefault();
}
If we add breakpoints to our plugin code:
And then 'Debug' TestMethod1():
The breakpoints in our plugin will be hit, and we can inspect what the plugin is doing and how it behaves against a real organization service.