Using interfaces as arguments in ZeroTouch nodes - or - how to unittest database calls?

Hi,
I’m building a collection of ZeroTouch nodes to connect and interact with MongoDB.
The basic nodes are developed, and I would like to add some unit tests to make sure they don’t break in the future.
Inspired by this post (and the other in the series), I wanted to use interfaces where I can, so that I can swap the Database Collection with a mock in my tests.

For example, for the the InsertMany node:

    public static string InsertMany(
        MongoCollectionBase<BsonDocument> collection,
        List<DesignScript.Builtin.Dictionary> dictionaries)
    {
        try
        {
            var documents = dictionaries.Select(x => Bson.ByDictionary(x));
            collection.InsertMany(documents);
            return "Success.";
        }
        catch (Exception)
        {
            return "Failed.";
        }
    }

I would like to declare my collection argument as IMongoCollection and then use a fake implementation of the collection for the tests.
The problem is if I change the type of the argument, then dynamo complains when I connect MongoCollection.ByName node (that outputs an object of type MongoCollectionBase and can’t output an interface) to it.

How can I accomplish this? The only thing that I can think of is create private functions with the interfaces, perform the unit tests on them and wrap them in public functions (nodes). But it seems to me that is too overkill for simple nodes (most of them are one liners).

I think I recently ran into this as well, and it appears to be a real limitation of the ZT importer - can you try a base class instead of an interface to see if that helps?

please also file this on the dynamo core gihub.

One thing I should have mentioned before: I’m still developing on/running Dynamo 2.0.3 because of company constraints (the BIM team is slowly moving from 1.3 to 2, and didn’t have time to check the compatibility of our workflows with newer dynamo versions).

I tried to use base classes, not in a testing context but in a real case scenario:

2020-03-10 18_25_56-Dynamo

UpdateOneModel<BsonDocument> is a subclass of WriteModel<BsonDocument>.

I’ll check if it’s still the case with the new version prior to file an issue.

ahh, would be useful to test this in the context of 2.x just to see if there are any differences - you can likely use DynamoSandbox.

Are both the derived class and the base class defined in the imported zero touch assembly? Or are they defined in some other assembly the ZT assembly references? I would try moving them inside the assembly as a sanity check.
OR defining another dummy class deriving from them so that they are definitely imported into the VM.

Also, I guess having the actual class hierarchy/code would be helpful here.

maybe you can declare them as object first? then do internal casting and throw your own custom exception. Likewise for your previous question. I believe you have to do hard casting for all your inputs and outputs to match. One suggestion i can give you, in order to previous these issue is to create your own wrapper class, which handles internal casting.

Thanks for the feedbacks.

I took the time to better analyze the ZeroTouch development wiki page and had a look at the DynamoSamples, and I started to understand how to organize my classes.

I didn’t get that Dynamo can build the nodes from instance methods, so I was creating only static methods with the class instance as first parameter.

By wrapping the MongoDB objects in my own classes I solved the second problem, now BulkWrite accepts all the wrapper classes I built (that inherits from the abstract base class defined as argument).

Still need to think about how to mock/replace the wrapped mongo collection for the tests, but it’s a great improvement from yesterday!

Thanks a lot!

maybe can share with us where did you followed the tutorial from? im also interested in developing a package for mongoDB but do not know how or where to start.

To answer your question about wrapping mongo collection, you can take look at Dynamo Revit source code as to how they wrap revit elements to theirs. The concept should be similar. Example:

public class MyMongoCollection<T>
{
    private IMongoCollection<T> _internalMongoCollection;
    public IMongoCollection<T> GetIMongoCollection()
    {
        return _internalMongoCollection ?? null;
    }
    public MongoCollectionBase<T> GetMongoCollectionBase()
    {
        return ((MongoCollectionBase<T>)_internalMongoCollection) ?? null;
    }
    internal MyMongoCollection(IMongoCollection<T> mongoCollection)
    {
        _internalMongoCollection = mongoCollection;
    }
}

the codes are typed on the fly so there is sure to be mistake but you get the jizz :slight_smile:

You can even edit your own custom output by override the ToString() method to provide user a more informative information, something like this:

    private string _collectionName
    {
        get { return _internalMongoCollection.CollectionNamespace.CollectionName; }
    }
    private double _collectionSize
    {
        get { return _internalMongoCollection.CountDocuments(FilterDefinition<T>.Empty); }
    }
    public override string ToString()
    {
        if (_collectionName == null || _collectionSize == default(double))
            return base.ToString();
        return string.Format("Name: {0}, Size{1}", _collectionName, _collectionSize);
    }

The page I was refferring to is Zero Touch Plugin Development on Dynamo GitHub wiki, and the samples contained in the DynamoSamples repository.

The wiki page says that you cannot use Generics, so your example will not run on Dynamo. I had to force BsonDocument as type (I’m going on the schemaless route here to make it as flexible as possible, so it’s not a problem).

My MongoCollection now looks like this:

public class MongoCollection
{
    private readonly MongoCollectionBase<BsonDocument> collection;

    private MongoCollection(MongoDatabase database, string name)
    {
        collection = (MongoCollectionBase<BsonDocument>)database.Database.GetCollection<BsonDocument>(name);
    }

    public static MongoCollection ByName(
        MongoDatabase database, string name)
    {
        return new MongoCollection(database, name);
    }
}

(Here MongoDatabase is another wrapper class for MongoDatabaseBase that exposes the actual object as Database property)

But you’re right, I can use the interface internally to be able to switch from MongoCollectionBase to my mocked class.

oh nono, dont get me wrong here. The class above should not be used as a “node” in dynamo, it is used to represent an “dynamo” object in dynamo environment. Its like the wrapper of Revit.Elements.Element and Autodesk.Revit.DB.Element in dynamo. Well of course you can nested static methods in the class just like how dynamo did, if you snooped around their source code, but i wouldnt do that due to clarity sake :smiley:

So now you should have two class, one is the MongoDBWrapper class, which in my example is the MyMongoCollection with the generics, and the other class with whatever class you wanna call that stores all the static method that is to be shown in dynamo as nodes. Hope you get what i mean as i really do not know how to show you an actual example with zero knowledge about MongoDB hahaha

EDIT: ah i think i misunderstood you and you misunderstood me… hahahha. But you get the hang of it! quick question, will you release these set of nodes as a packages to the public?

Thanks @stillgotme for the clairfication, it all makes sense now :slight_smile:

I finally got to the point where I can mock the database/collection without the need to create a custom interface. The Collection property (it was a field before) could as well be of an interface type, so I can swap the MongoCollectionBase with a Mock<IMongoCollection>:

public class MongoCollection
{
    internal IMongoCollection<BsonDocument> Collection { get; private set; }

    // this is only called in unittests
    internal MongoCollection(IMongoCollection<BsonDocument> collection, string name)
    {
        Collection = collection;
    }

    // this is called by the ByName staticmethod
    internal MongoCollection(IMongoDatabase database, string name)
    {
        Name = name;
        Collection = database.GetCollection<BsonDocument>(name);
    }

    public static MongoCollection ByName(
        MongoDatabase database, string name)
    {
        return new MongoCollection(database.Database, name);
    }

    // method to test
    public string InsertMany(
        List<DesignScript.Builtin.Dictionary> dictionaries)
    {
        try
        {
            var documents = dictionaries.Select(x => Bson.ByDictionary(x));
            Collection.InsertMany(documents);
            return "Success.";
        }
        catch (Exception)
        {
            return "Failed.";
        }
    }
}

Then in the unittest i use Moq to mock the interfaces:

    [Test]
    [Category("UnitTests")]
    public void CanInsertManyDicts()
    {
        var collectionMock = new Mock<IMongoCollection<BsonDocument>>();
        collectionMock.Setup(x => x.InsertMany(
            It.IsAny<IEnumerable<BsonDocument>>(), null, default));
        var collection = new MongoCollection(collectionMock.Object, "TestCollection");
        var dictionaries = new List<Dictionary>
        {
            Dictionary.ByKeysValues(
            new List<string>() { "Just", "A", "Test" },
            new List<object>() { "Seems", "running", "fine" }
            )
        };
        var result = collection.InsertMany(dictionaries);
        Assert.AreEqual("Success.", result);
    }

To answer your last question: I’m developing this for my company, I’ll have to check if I’m allowed to publish it or not… but in COVID-19 times, this is a low priority issue for my manager :face_with_thermometer: