Unit Testing Revit Add-ins


Introduction
One of the biggest pain points developers face when developing add-ins for Autodesk Revit is the slow manual process of testing their changes. Each time a change is made, the developer is often forced to launch Revit, load up a test file and navigate through their UI, just to test a small piece of functionality. This eats up a significant amount of time throughout the day.
The root of the problem lies in the tight coupling of application logic with the Revit API, which makes it nearly impossible to isolate and test code outside of Revit’s runtime environment.
In this blog post, we’ll explore why unit testing is so challenging for Revit add-ins, and how FORMlab structures their Revit add-in codebases to allow for testing application logic outside of Revit.
Solution Hierarchy
FORMlab Revit add-ins adhere to the following project structure in their C# solutions to isolate as much code as possible from the Revit API:
Form.<Name>.Bim
This project can be seen as: the add-in’s understanding of the Revit API.
It is a collection of abstractions (mostly interfaces), that are used in the codebase to interact with Revit. These abstractions contain everything the add-in requires of Revit functionality (and nothing more!). Often, the interactions (methods / properties) will have been simplified to allow for more readable/mainainable domain logic. This project does not reference Revit API dll’s.Form.<Name>.Bim.Revit
This project is responsible for transforming the abstractions inForm.<Name>.Bim
to actual interactions with Revit through the Revit API dll’s. This implementation strategy can be referred to as a software pattern called: Facade Pattern. We’re aiming to simplify Revit API interactions to the bare minimum the add-in requires, effectively creating a facade our code interacts with.Form.<Name>.Core
Core domain logic resides here. Services, domain models, anything needed for the add-in to do actual useful work. This project does not reference Revit dlls: the vital aspect to allow for unit testing.
Instead, it references theForm.<Name>.Bim
project, and takes what is defined in there as the inputs for its services.Form.<Name>.Ui
Any UI that the add-in has is here. This project does not reference Revit dlls.Form.<Name>.Addin
The main project, i.e. the addin entry point, containingStartup.cs
.
This project references all other projects listed above, as well as the Revit dll’s.
The following coverage result of one of FORMlab’s Revit add-ins illustrates how testable code correlates to coupling with Revit API dll’s:
Bim.Revit
cannot be tested, as there’s nothing but Revit API coupled code here..Addin
mostly interacts directly with Revit, but has some testable areas (more on that below)..Ui
can be tested pretty decently. However, as UI has it’s own problems, discussing it is outside the scope of this article..Core
is fully testable..Bim
does not contain (a lot of) logic and will almost be fully covered by tests of.Core
.
From Entry to Outcome
Now the question remains, how are the services in these various projects connected?
The main entry points from Revit to our own code are usually ExternalCommand
's. They serve as the link between Revit and testable code. Let’s look at an example:
namespace Form.Configurator.AddIn.Commands.ConfigureBlock;
/// <summary>
/// Launch a window for the user to configure a block
/// </summary>
[Transaction(TransactionMode.Manual)]
public class ConfigureBlockCommand : IExternalCommand
{
public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
IUiApplication app = new RevitUiApplication(commandData.Application);
ConfigureBlockCommandImpl command = new(app, Main.Instance.Container);
bool success = command.Execute();
return success ? Result.Succeeded : Result.Failed;
}
}
An actual implementation of
IExternalCommand
is required, and is not testable. It’s goal is to pass control over to testable code as fast as possible, while containing as little logic as possible.This command converts a required Revit object from the given
ExternalCommandData
to a version from the.Bim
project. In the above case, this is the conversion from Revit’sUIApplication
to our ownIUiApplication
. The concrete implementation from theBim.Revit
project is used for this conversion:
RevitUiApplication : IUiApplication
.This object is passed into a testable version of the command (
…CommandImpl
), that doesn’t interact with the Revit API anymore.From there, the application flow no longer interacts with the Revit API directly. It only interacts with facade objects through the defined abstractions in
.Bim
.
As a final illustration, let’s consider a concrete example of what you might want to unit test.
Consider the following code:
public ProcessResult Process(IDocument doc, ProcessSettings settings)
{
using ITransaction transaction = doc.GetTransaction();
try
{
transaction.Start("Process");
ProcessResult result = _domainLogicProcessor.DoWork(doc, settings);
transaction.Commit();
return result;
}
catch (Exception exc)
{
if (transaction.HasStarted)
transaction.RollBack();
return new FailedProcessResult(exc);
}
}
By interacting with our own definitions: IDocument
& ITransaction
, we are free to unit test this code for all the behaviour we care about, like: whether a transaction is started, whether it gets rolled back when processing fails, and whether the transaction gets committed when processing was successful.
Closing Notes
A successful implementation of this strategy can be thought of in the following way:
Let’s say the addin needs to be ported over to another BIM software package, and you want to maintain both addins from the same codebase. The strategy is successful if this can be easily done by creating a separate .Addin
project for the other software, and a separate .Bim.OtherSoftware
project for the conversion of .Bim
objects to the new software’s API. The remaining projects are untouched and work for both addins!
There’s more to explore around this topic, with subjects like:
- How to write the implementations in .Bim.Revit
.
- How to write the unit tests themselves.
- How to create test doubles (i.e. mocks/stubs) of the .Bim
abstractions.
- How to make sure this extra setup creates more benefit than it costs by maximizing maintainability.
If this article manages to peak some interest, we’ll gladly dive into these subjects in further installments. So please let us know!
Whether your needs involve Revit add-ins, Dynamo scripts, Windows applications, or web-based tools, we at FORMlab craft software that seamlessly integrates into your workflow. Get in touch to explore how we can bring your ideas to life!
📧 formlab@form.nl
🌐 formhet.nl/technologie
🔗 FORMlab on LinkedIn
Subscribe to my newsletter
Read articles from FORMlab directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
