Unit tests for Maya C++ plugins with Google Test

Unit testing Maya C++ Plugins can be very challenging, but learning this aspect can significantly enhance your development workflow. This guide aims to demystify the process of unit test creation for Maya C++ plugins using Google Test and Visual Studio, paving the way for your first successful test.

Prerequisites

Ensure Visual Studio is set up for plugin development and integrated with a Google Test project. If you're new to this setup, consider following this tutorial.

Objective

We'll build a simple class to unit test the core logic of the "CenterPoint" plugin.

Plugin Overview

The "CenterPoint" plugin is intended to be a very simple use case for learning purposes and may not be exactly useful for production. Its main functionality can be easily achieved using standard Maya nodes.

Focus Area

Our primary focus will be the CenterPointNode class, which embodies the plugin's logic. For a complete view of the code and further details, please refer to our GitHub repository.

//CenterPointNode.cpp
#include "CenterPointNode.h"
static const MTypeId TYPE_ID = MTypeId(0x0007F7F3);
static const MString TYPE_NAME = "centerPoint";
MObject CenterPointNode::inputObjectsAttr;
MObject CenterPointNode::outputPosAttr;
CenterPointNode::CenterPointNode() 
{
}
MStatus CenterPointNode::compute(const MPlug& plug, MDataBlock& data)
{
    if (!isDirty(plug))
    {
        return MS::kSuccess;
    }
    std::vector<MVector> positions;
    MStatus result;
    MArrayDataHandle arrayDataHandle = data.inputArrayValue(inputObjectsAttr, &result);
    if (!result)
    {
        MGlobal::displayError("Error getting the input value!");
        return MS::kFailure;
    }
    result = arrayDataHandle.jumpToArrayElement(0);
    if (!result)
    {
        MGlobal::displayError("Error getting the first element of the inputs!");
        return MS::kFailure;
    }

    do
    {
        MDataHandle inputDataHandle = arrayDataHandle.inputValue(&result);
        if (!result)
        {
            MGlobal::displayError("Error getting value of the input!");
            return MS::kFailure;
        }
        MMatrix worldMatrix = inputDataHandle.asMatrix();
        MTransformationMatrix trasnformationMatrix(worldMatrix);
        MVector translation = trasnformationMatrix.getTranslation(MSpace::kWorld, &result);
        if (!result)
        {
            MGlobal::displayError("Error getting the translation value of the input!");
            return MS::kFailure;
        }
        positions.push_back(translation);
    } while (arrayDataHandle.next() == MS::kSuccess);
    MVector centerPoint = findCenterPoint(positions);
    MDataHandle outputDataHandle = data.outputValue(outputPosAttr, &result);
    if (!result)
    {
        MGlobal::displayError("Error getting the output data handle!");
        return MS::kFailure;
    }
    outputDataHandle.set3Double(centerPoint.x, centerPoint.y, centerPoint.z);
    data.setClean(plug);
    return MS::kSuccess;
}
void* CenterPointNode::Creator()
{
    return new CenterPointNode();
}
MStatus CenterPointNode::Initialize()
{
    defineAttributes();
    return MS::kSuccess;
}
MTypeId CenterPointNode::GetTypeId()
{
    return TYPE_ID;
}
MString CenterPointNode::GetTypeName()
{
    return TYPE_NAME;
}
bool CenterPointNode::isDirty(const MPlug& plug)
{
    return plug == outputPosAttr;
}
MVector CenterPointNode::findCenterPoint(std::vector<MVector> positions)
{
    MVector sum_vector(0.0f, 0.0f, 0.0f);
    for (MVector pos : positions)
    {
        sum_vector += pos;
    }
    MVector center_point = sum_vector / positions.size();
    return center_point;
}
void CenterPointNode::defineAttributes()
{
    MFnMatrixAttribute matrixAttrFn;
    inputObjectsAttr = matrixAttrFn.create("input", "in", MFnMatrixAttribute::kDouble);
    matrixAttrFn.setArray(true);
    matrixAttrFn.setStorable(true);
    matrixAttrFn.setKeyable(true);
    matrixAttrFn.setReadable(false);
    matrixAttrFn.setWritable(true);
    addAttribute(inputObjectsAttr);
    MFnNumericAttribute numericAttrFn;
    outputPosAttr = numericAttrFn.create("outputPosition", "op", MFnNumericData::k3Double);
    numericAttrFn.setStorable(false);
    numericAttrFn.setReadable(true);
    numericAttrFn.setWritable(false);
    addAttribute(outputPosAttr);
    attributeAffects(inputObjectsAttr, outputPosAttr);
}
CenterPointNode::~CenterPointNode()
{
}

Testing Philosophy for C++ Plugins

Creating unit tests for C++ plugins can be much more challenging when compared to creating tests for Python plugins, especially if mocking is required. However, when testing in C++, due to the extreme complexity of mocking Maya operations, I usually advocate for testing only the functions responsible for the main operations of the plugin, leaving Maya dependencies outside of the scope when possible. Therefore, for C++ plugins, I believe it makes more sense to cover all possible behaviors of the plugin through integration tests using Python and pytest rather than C++ unit tests (for an example of integration tests using pytest check this repo). This post will not cover integration tests.

The plugin contains only two main "testable" files: the main.cpp file, which contains the code to initialize/uninitialize the plugin, and the CenterPointNode class. For now, we're going to focus only on the CenterPointNode class.

#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <vector>
#include <maya/MVector.h>
#include "../CenterPointNode.h"

TEST(TestCenterPointNode, TestFindCenterPoint)
{
    CenterPointNode centerPointNode;
    std::vector<MVector> inputPositions = {
        MVector(0.0, 0.0, 0.0),
        MVector(0.5, 10.3, 50.8),
        MVector(-15.5, -8.0, -30.0),
        MVector(34.4, 56.66, -45.0),
    };

    MVector expectedResult(4.850, 14.740, -6.050);
    MVector actualResult = centerPointNode.findCenterPoint(inputPositions);

    float tolerance = 0.01f;
    ASSERT_NEAR(expectedResult.x, actualResult.x, tolerance);
    ASSERT_NEAR(expectedResult.y, actualResult.y, tolerance);
    ASSERT_NEAR(expectedResult.z, actualResult.z, tolerance);
}

int main(int argc, char **argv)
{
    testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

This test validates the findCenterPoint() method's accuracy by comparing the expected results with actual outcomes, using a tolerance level to account for rounding discrepancies. Given four initial positions (0.0, 0.0, 0.0), (0.5, 10.3, 50.8), (-15.5, -8.0, -30.0), (34.4, 56.66, -45.0), the function will test if the findCenterPoint method of the plugin returns the correct center point (4.850, 14.740, -6.050).

Challenges and Solutions

Testing the compute() function directly would ideally provide more robust validation. However, the intricate dependencies on Maya's MDataBlock complicate this approach. Similarly, the defineAttributes() method, crucial for plugin functionality, is not directly tested due to the complexity of mocking Maya API dependencies. These limitations underscore the value of complementary integration testing.

Conclusion

While unit tests may not cover all logical aspects due to the inherent complexities of Maya's C++ API, focusing on key functionalities can significantly improve development efficiency and code quality.

For access to the full codebase, including setup instructions, visit our repository.