Make a CCG – Aspect Container & Test Runner

Whenever I begin a new project, the first thing I think about is how I want to set up the architecture. Basically I am looking for the answer to this question: “How will all the stuff know about all the other stuff?”. In the past I usually resorted to a pattern called the Singleton to make it easy for data to be accessible from anywhere. In this case, I don’t want to rely on Singletons because I want my code to be testable, and from what I have read, the two don’t tend to go together well.

If you haven’t already grabbed a copy of the starter project, then go ahead and download a copy and open it. We will be building up on this as we go. I have also prepared an online repository here for those of you familiar with version control.

Aspect Container

For this project I was inspired by Unity’s GameObject to MonoBehaviour relationship. I really like how easy it is for a GameObject to get a reference to any component attached to it. I also like that any component can also get a reference to any other component on the same object since they also hold a connection to the GameObject that holds them. However, I am sure many of you are well aware of the many limitations associated with MonoBehaviour scripts, such as an inability to instantiate them on your own. For example, the code in the snippet below would compile and run, but Unity will compain at us:

[csharp]
public class Foo : MonoBehaviour {

}

public class Demo : MonoBehaviour {
void Start () {
var foo = new Foo ();
}
}
[/csharp]

You are trying to create a MonoBehaviour using the ‘new’ keyword. This is not allowed. MonoBehaviours can only be added using AddComponent(). Alternatively, your script can inherit from ScriptableObject or no base class at all

The Test Runner in Unity supports PlayMode testing, and so it could be used to test MonoBehaviour scripts. However, I think we will want to make a LOT of tests, and will want as much speed and control as possible. Ideally, all of our tests will operate under EditMode instead. Therefore, I will make the code independent of Unity architecture anywhere that I can.

The implementation for my solution was to make my own circular reference system similar to the GameObject pattern provided by Unity. In order to avoid confusion to myself and others, I felt it was important to come up with new names. The GameObject was implemented as an interface, “IContainer” and the Component was implemented as an interface “IAspect”. However, when used in a project even with just some of my own other scripts I found I had already run into a naming conflict. This is because I had also used an “IContainer” interface with my Scripts/Common/UI/TableView. So one thing you might notice that has changed in this new project is the use of namespaces. All of my code in the “Common” folder has been put into a unique namespace. That includes this new Aspect Container code, were I am using [csharp]namespace TheLiquidFire.AspectContainer[/csharp] to help alleviate naming conflict issues.

Although the aspect container could be used just about anywhere, I only intend to use it as a means of allowing all of my systems to communicate between each other. This means I wont be worried about a container having more than one aspect of any given type. Because of this, I designed something that looks a bit like a dictionary (as opposed to a list), except that in this case, each “value” would know what it was collected by.

[csharp]
public interface IContainer {
T AddAspect (string key = null) where T : IAspect, new ();
T AddAspect (T aspect, string key = null) where T : IAspect;
T GetAspect (string key = null) where T : IAspect;
ICollection Aspects ();
}

public interface IAspect {
IContainer container { get; set; }
}
[/csharp]

I made each interface as simple as possible. The container needs a way to add an aspect, get an aspect that has been added to it, or get all of the aspects it currently holds. The aspect merely needs a property reference to the container that it is held by.

Note that in all of the methods for adding or getting an aspect from a container, that there is an optional parameter for the “key”. In each case, you can specify a custom string to use, or you can allow the implementing system to generate a key for you. The important detail is that if you “add” with a key, then you should be able to “get” with the same key. If you don’t “add” with a key, then you should be able to “get” without a key as well.

You may wonder why I added two methods for adding an aspect. The first will create an instance of the aspect for you. This is possible because of the added generic constraint: [csharp]new()[/csharp]. The second option allows you to pass along a pre-existing aspect. This could be important for any scenario where it didn’t make sense to support a parameterless constructor, such as if you needed a MonoBehaviour to implement the IAspect interface. It also allows for a kind of dependency injection. For example, if you create all of your aspects according to an interface, then you can easily create an alternate or mock version to use later. That might look something like this:

[csharp]
public interface IFoo : IAspect {

}

public class Foo : IFoo {
public IContainer container { get; set; }
}

public class Demo : MonoBehaviour {
void Start () {
var container = new Container ();
container.AddAspect (new Foo ());

IFoo foo = container.GetAspect ();
Debug.Log (“Got a foo? ” + (foo != null).ToString());
}
}
[/csharp]

Test Driven Development

My own introduction to Test Driven Development (TDD) was kindly provided by this excellent tutorial. Note that this is for Xcode and uses Swift – which is perfect for what I do at work, but may not be as helpful for those of you who are only familiar with Unity and C#. So, I will give my own sort of intro to the idea as I understand it. TDD suggests a certain workflow whereby you begin your work by writing a test – not your actual code. We write this test first because we want to see the test fail. Then, when we add actual code which allows the test to pass, we can have confidence that it was the new code which caused the test to pass, and that it was not simply a poorly written test that might have passed anyway. Afterward we are free to tidy up the code and refactor as necessary. If the test can still pass after refactoring then we know we haven’t lost any expected functionality.

I haven’t ever done a project from scratch using this kind of workflow, and I am not sure I have the patience for it honestly, but a large part of that is because I am already an experienced programmer and I like to get in quick and “doodle” with my code for lack of a better term. Even still, I can see how this pattern would benefit even experienced programmers (myself included) – especially those who are working on large projects and/or with other people, so I want to try it out – maybe not perfectly strictly, but enough to get a good idea about it.

Container

Let’s get started by creating a “Container” class which implements our “IContainer” interface. Here is the least amount of code I can add – actually I let MonoDevelop write the code for me. Once I created the class definition, you can right-click on the name of the interface and choose Refactor -> Implement Interface which gives a result like the following:

Implement Interface

[csharp]
public class Container : IContainer {
public T AddAspect (string key = null) where T : IAspect, new() {
throw new System.NotImplementedException ();
}

public T AddAspect (T aspect, string key = null) where T : IAspect {
throw new System.NotImplementedException ();
}

public T GetAspect (string key = null) where T : IAspect {
throw new System.NotImplementedException ();
}

public ICollection Aspects () {
throw new System.NotImplementedException ();
}
}
[/csharp]

Now, we’re ready to write our first test for the new “Container” class. You can do this easily from the Project Pane inside of Unity. From the Create pulldown menu, choose Testing -> EditMode Test C# Script. I named mine, “AspectContainerTests” and the template looks like this:

Add Test Script

[csharp]
public class AspectContainerTests {

[Test]
public void AspectContainerTestsSimplePasses() {
// Use the Assert class to test conditions.
}

// A UnityTest behaves like a coroutine in PlayMode
// and allows you to yield null to skip a frame in EditMode
[UnityTest]
public IEnumerator AspectContainerTestsWithEnumeratorPasses() {
// Use the Assert class to test conditions.
// yield to skip a frame
yield return null;
}
}
[/csharp]

Note that we use special tags above methods to make the TestRunner work. In this case, they have provided entries for two different kinds of tests, the first is marked with [csharp][Test][/csharp] – this one will run and return immediately. Use assertions to validate “something” and when the assertion fails, your test will be marked as a failure too. The next test is similar to the first but is marked with [csharp][UnityTest][/csharp] and as the comment suggests, it can skip frames to help simulate behavior like you might see in a PlayMode test.

There are several other tags you can mark your methods with, such as [csharp][SetUp][/csharp] – which will be run immediately before each unit test, and [csharp][TearDown][/csharp] – which will be run immediately following each unit test. I learned about these from the NUnit Documentation that Unity built its tools around. Of course you should probably also check out Unity’s TestRunner documentation which has screen grabs from the Unity interface and several suggested options you can try out.

For now let’s clear out all of the template test code and replace it with the following:

[csharp]
using UnityEngine;
using UnityEditor;
using UnityEngine.TestTools;
using NUnit.Framework;
using System.Collections;
using TheLiquidFire.AspectContainer;

public class AspectContainerTests {

private class TestAspect : IAspect {
public IContainer container { get; set; }
}

[Test]
public void TestContainerCanAddAspect() {
var container = new Container ();
container.AddAspect ();
Assert.AreEqual (container.Aspects().Count, 1);
}
}
[/csharp]

The Test Runner panel will update to dsiplay our tests as we create them. Currently they won’t be marked as either a pass or a fail. Now, using the TestRunner interface, begin an EditMode test by pressing the “Run All” button. The result will be that our test failed, but don’t worry – this is a good thing – it’s the first thing we want to see in a TDD workflow.

Add Aspect Fail

Now, we need to start adding code in order to allow the test to pass. Head back over to the “Container” class. If you follow the TDD pattern strictly, they suggest that you add the least amount of code possible in order to make a test pass – this is true even when you KNOW that the code is terrible. This is the part that really bugs me, but again, there is a method to the madness. The code will become better over time as you add more tests. In this case, I will need to be able to create an aspect, and add it to a collection so that I can return the populated collection in my test. For the first step, I will have my container implement a private Dictionary which can map between string key’s and an IAspect instance. Add the following field to the “Container” class:

[csharp]
Dictionary aspects = new Dictionary ();
[/csharp]

Next, I will want to modify the “AddAspect” method so that it actually creates something and adds it to our collection:

[csharp]
public T AddAspect (string key = null) where T : IAspect, new() {
T aspect = new T ();
aspects.Add (string.Empty, aspect);
return aspect;
}
[/csharp]

Finally, we need to modify the “Aspects” method so that it returns the collection of aspects:

[csharp]
public ICollection Aspects () {
return aspects.Values;
}
[/csharp]

Save and build your code. Then head back to Unity and run TestRunner again. This time, the test should pass. However, the code currently is pretty bad and wont work for a lot of scenarios. That’s ok. That’s what more tests are for. Let’s write a test to make sure that we can now add more than one aspect to a container. My expectation is that Aspects added to a container will either have unique keys, or unique types. In this case we will test via unique keys. Head back to the “AspectContainerTests” script and add the following:

[csharp]
[Test]
public void TestContainerCanAddMultipleAspects() {
var container = new Container ();
container.AddAspect (“Test1”);
container.AddAspect (“Test2”);
Assert.AreEqual (container.Aspects().Count, 2);
}
[/csharp]

You can test it by choosing “Run All” again, but since we just ran the previous test and already know it works, we can be a little more efficient by selecting just the new test, and pressing “Run Selected” instead. The result will be a failure, and that failure will propagate up through the hierarchy to make sure you see that something failed.

Add Multiple Fail

Now we have to wonder at why our test worked for adding one aspect, but not for adding more than one. The reason is that I have the dictionary add the aspect using an empty string as its key. This was the “least” I could do to make the code run (because adding a null key would result in an System.ArgumentNullException – you can’t add null as a Dictionary key). Because we always use the same key, the Dictionary’s “Add” method will then throw an exception because the key already existed: System.ArgumentException : An element with the same key already exists in the dictionary.

In order to make the new test pass, we need to use a different “key” for each of the aspects which get added. My new test already provides a custom key to use for each aspect, therefore the simplest update to make is to pass along the value as the key for our Dictionary. Update the “Container” method as follows:

[csharp]
public T AddAspect (string key = null) where T : IAspect, new() {
T aspect = new T ();
aspects.Add (key, aspect);
return aspect;
}
[/csharp]

Save and Build your code, then let’s go ahead and perform a “Run All”. This time, our newest test succeeds, but our first test fails! I already hinted at the reason – you can’t add a null key, and in the first test we leave the optional parameter for the key at its default of null. Update the “Container” method by adding a check for a null key as follows:

[csharp]
public T AddAspect (string key = null) where T : IAspect, new() {
key = key ?? “”;
T aspect = new T ();
aspects.Add (key, aspect);
return aspect;
}
[/csharp]

Let’s run our tests again. They all pass! But… the code still isn’t versatile enough. I mentioned before that my expectation was that aspects should either have unique keys or unique types, so let’s add a test for that too. Let’s add another test that adds different types of aspects – without passing a custom key. Add the following to your “AspectContainerTests” script:

[csharp]
private class AltTestAspect : IAspect {
public IContainer container { get; set; }
}

[Test]
public void TestContainerCanAddMultipleTypesOfAspects() {
var container = new Container ();
container.AddAspect ();
container.AddAspect ();
Assert.AreEqual (container.Aspects().Count, 2);
}
[/csharp]

Save and build, then run the new test. It too will fail. What this will point out is that merely checking for a null key and replacing it with an empty string is not sufficient. Each “Type” that can be passed along will need a unique key. We could force the “IAspect” interface to provide us a default key, but there is another option too. The name of the type is already unique and can be used as our fallback instead. Update our “Container” method’s null check to the following:

[csharp]
key = key ?? typeof(T).Name;
[/csharp]

Save, build, and run our tests – they should all pass. However, adding an aspect isn’t very valuable without the ability to get the aspect again later. Let’s add two new tests: first to verify that an aspect added without a key can be obtained later without a key, and second to verify that an aspect added with a custom key can be obtained using that same key.

[csharp]
[Test]
public void TestContainerCanGetAspectWithNoKey() {
var container = new Container ();
var original = container.AddAspect ();
var fetch = container.GetAspect ();
Assert.AreSame (original, fetch);
}

[Test]
public void TestContainerCanGetAspectWithKey() {
var container = new Container ();
var original = container.AddAspect (“Foo”);
var fetch = container.GetAspect (“Foo”);
Assert.AreSame (original, fetch);
}
[/csharp]

These tests will both fail. At a minimum, we need to remove the NotImplementedException provided by the default code we generated. Remember, we want to write the least amount of code possible to make our tests pass. Let’s begin with the following attempt:

[csharp]
public T GetAspect (string key = null) where T : IAspect {
return aspects [key];
}
[/csharp]

This will allow the test “with” a key to pass, but not the test without a key. This may remind us that we forgot to account for a null key. Add the following line to our “GetAspect” method just before the return statement:

[csharp]
key = key ?? typeof(T).Name;
[/csharp]

It’s the same code we used to account for null keys when adding an aspect, so it makes sense that we would want to add it here. Run the tests again, and they should all pass.

Sometimes you may not think of all of the tests necessary to bullet-proof your code. If you are lucky, you may have a QA team to test your project. Let’s imagine a scenario where some unexpected order of operations causes code to try and “Get” an Aspect before it has been added to the container. Using a stack trace you identify the source of the problem as a “KeyNotFoundException” and can pinpoint it to your codes “GetAspect” method. Whoops. We will need to add another test:

[csharp]
[Test]
public void TestContainerCanTryGetMissingAspect() {
var container = new Container ();
var fetch = container.GetAspect (“Foo”);
Assert.IsNull (fetch);
}
[/csharp]

Save, build and run the new test. This test fails AND triggers the same exception that our QA team had stumbled upon, and so it will be perfect to verify whether or not we have successfully fixed the bug. Head back over to the “Container” and update our method to the following version. This time we check to make sure that a key exists in the dictionary before we try to use it. Whenever the key is missing, we can return the default of the type we want – for a class it will be null.

[csharp]
public T GetAspect (string key = null) where T : IAspect {
key = key ?? typeof(T).Name;
T aspect = aspects.ContainsKey (key) ? (T)aspects [key] : default (T);
return aspect;
}
[/csharp]

Success! Our tests pass once again. Now we need to add something to test the final feature of our Container – the ability to add a pre-existing aspect. Add the following tests:

[csharp]
[Test]
public void TestContainerCanAddPreCreatedAspect() {
var container = new Container ();
var aspect = new TestAspect ();
container.AddAspect (aspect);
Assert.IsNotEmpty (container.Aspects());
}

[Test]
public void TestContainerCanGetPreCreatedAspect() {
var container = new Container ();
var original = new TestAspect ();
container.AddAspect (original);
var fetch = container.GetAspect ();
Assert.AreSame(original, fetch);
}
[/csharp]

Running the tests now will fail, of course, because we still haven’t implemented anything and we throw the same NotImplementedException as before. Feel free to test that for yourself if you wish, otherwise, let’s make the simplest “change” we can by copying the implementation we used for the first version of AddAspect. Of course, a straight copy of the method body wont work because we would get an error: error CS0128: A local variable named `aspect’ is already defined in this scope, so we will need to delete the line where we create the new aspect. The end result should look like the following:

[csharp]
public T AddAspect (T aspect, string key = null) where T : IAspect {
key = key ?? typeof(T).Name;
aspects.Add (key, aspect);
return aspect;
}
[/csharp]

At this point, we have a fully validated class. We can proceed with a high level of confidence that our class will work as expected when we begin using it in our project! But… we aren’t done yet. There are three steps to properly practice TDD:

  1. Write a failing test
  2. Write as little code as possible to make the test pass
  3. Polish your code

We have only performed the first two steps. The final step is to take another look over our code and see if there is anything we should do to improve on it. If so, we should make the changes, and then run the tests yet again to verify that we haven’t lost any of our expected functionality in the process. The first thing that jumps out to me is that code should be DRY (this stands for “Don’t repeat yourself”) yet that is exactly what we did to implement the second “AddAspect” method. We can clean this up by causing the first method to call the second, and then we will only need one version. That would look something like this:

[csharp]
public T AddAspect (string key = null) where T : IAspect, new() {
return AddAspect (new T (), key);
}

public T AddAspect (T aspect, string key = null) where T : IAspect {
key = key ?? typeof(T).Name;
aspects.Add (key, aspect);
return aspect;
}
[/csharp]

Aspect

Just a little more and we will be done. First things first, we will need to provide a class which implements the IAspect interface:

[csharp]
public class Aspect : IAspect {
public IContainer container { get; set; }
}
[/csharp]

Couldn’t be much simpler than that. Next, let’s write a test. We will want to make sure that an Aspect instance will know what it is contained by. Let’s add the following test (hopefully you will remember to verify on your own that it fails):

[csharp]
[Test]
public void TestAspectTracksItsContainer() {
var container = new Container ();
var aspect = container.AddAspect ();
Assert.IsNotNull (aspect.container);
}
[/csharp]

Even though the aspect is created and is actually added to a collection, the reverse reference from an aspect to its container has not been configured anywhere. We can do this during the Container’s “AddAspect” method by assigning the reference before we return. Make sure to add this to the version which accepts both an aspect and a key as parameters so that it will work the same for either version.

[csharp]
public T AddAspect (T aspect, string key = null) where T : IAspect {
key = key ?? typeof(T).Name;
aspects.Add (key, aspect);
aspect.container = this;
return aspect;
}
[/csharp]

Summary

Whew, this was a long lesson – and we didn’t even write a large or complex class! We did cover a lot though. In the beginning I discussed a new architectural design pattern that I wanted to implement in order to let my systems work together, but without the need of having a bunch of singletons. Next, we actually implemented the new class that would support the pattern, but did so via Test Driven Development, while learning all about Unity’s new Test Runner feature.

Hopefully you understand the process of implementing good unit tests, because I don’t plan on writing all of my future lessons with the same style of back and forth as I did here. My main concern is the time requirement – not just to write the code and tests in the first place, but to write the tutorial as well. I would love to hear your feedback on this though – was one TDD lesson enough or should I continue to write that way for a little longer? Note that either way I plan to include the test code which will likely serve as a demo so you know things are working – especially before we get around to adding “visible” content via Unity.

You can grab the project with this lesson fully implemented right here. If you are familiar with version control, you can also grab a copy of the project from my repository.

If you find value in my blog, you can support its continued development by becoming my patron. Visit my Patreon page here. Thanks!

13 thoughts on “Make a CCG – Aspect Container & Test Runner

  1. Nice lesson!
    One question, we expect aspects to have unique keys or types and this should be granted by the Dictionary. If we try to Add twice a certain aspect type without a key, this should turn in an ArgumentException because of the duplicated key (type name).

    What if we decide to replace the Dictionary with another structure that (for unknown reasons) allows duplicated entries? Should we write a test to verify that you can’t add 2 same type without any key?

    Thanks again!

    Antonio

    1. Thanks, and great question. By my stated definition of an Aspect Container, then yes, an implementation that allowed duplicates should have an extra test to ensure that the rules were followed. Now that you mention it, I could have designed the tests around the interface rather than my actual implementation class and then I might have thought to add that test as well.

      Of course, if you wanted a container that didn’t use a dictionary, then you may not actually want to follow my initial definition of an aspect container and may want to allow duplicates. In that case it would look even more like Unity’s component pattern, but would probably also justify changing the IContainer interface since a “key” wouldn’t make as much sense.

  2. In the code section under “For now let’s clear out all of the template test code and replace it with the following:”

    The last code line before the closing braces:

    Assert.AreEqual (container.Aspects.Count, 1);

    should be:

    Assert.AreEqual (container.Aspects().Count, 1);

    Great tutorial so far, thanks for doing this!

    1. Thanks for the compliments! I’m definitely open to the idea of guest content – I tried contacting you but the email on your comment didn’t go through.

  3. Awesome Lesson, I really enjoyed it <3

    In the modification of TestContainerCanGetAspectWithKey we should use

    return (T)aspects[key];

    instead of :

    return aspects[key];

    and one thing else , I had a problem when I started to use tests, when you create test folder and create your very first test assembly , in your test class you cant access any of interfaces or namespaces, so you need to add an assembly of your scripts to Assembly Definition Resources in your test assembly(You can tweak it from Editor)(I'm using unity 2020.1.10f1)
    I thought it worth mentioning.

    After all thanks for awesome lesson and tutorial. Thumbs up

  4. And three and a half years later it’s still an insightful and well structured article… Thank you very much.

  5. I absolutely love this tutorial, I stumbled upon it while deeper into another project that I wanted to merge some card game aspects into. Do you offer any sort of 1:1 consultation services? I would love to spend an hour just talking with you about some of the modifications and questions I have on the things in this project and I’d be happy to pay.

Leave a Reply to admin Cancel reply

Your email address will not be published. Required fields are marked *