Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice

Humble Object Pattern: Unit Testing compatibility vs Integration Test compatibility

Discussion in 'Scripting' started by IDoAllTheWork, Nov 22, 2015.

  1. IDoAllTheWork

    IDoAllTheWork

    Joined:
    Jun 20, 2015
    Posts:
    19
    Hey folks,

    I'm trying to work with the Unity Test Tools. Reading through the blog posts I stumbled upon the "Humble Object Pattern", which helps you to separate logic from framework related stuff and makes unit testing a lot easier. But now that I have separated things, I find it harder to set up integration tests.

    E.g. I have a HealthBehaviour (MonoBehaviour child) and a HealthLogic (no MonoBehaviour child, contains logic). By moving the hit points field to my controller, I cannot access it with the AssertionComponent anymore. How do you deal with this problem? Creating a property in the HealthBehaviour is probably a possible solution, although I don't think it really leads to clean code.

    Or how do you in general use the Humble Object Pattern in your classes?

    Code (CSharp):
    1.  
    2. public class HealthBehaviour : MonoBehaviour, IDamageable {
    3.  
    4.     public HealthController _healthController = new HealthController();
    5.  
    6.     void Awake()
    7.     {
    8.         _healthController.SetHitpointsController(this);
    9.     }
    10.  
    11.     #region IDamageable implementation
    12.     public void TakeDamage(int damageTaken)
    13.     {
    14.         _healthController.TakeDamage(damageTaken);
    15.     }
    16.  
    17.     public void Kill()
    18.     {
    19.         Destroy(gameObject);
    20.     }
    21.     #endregion IDamageable implementation;
    22. }
    23.  
    Code (CSharp):
    1.  
    2. public class HealthController {
    3.  
    4.     public readonly int _initialHitPoints = 100;
    5.     public int CurrentHitPoints {get{return _currentHitPoints;}}
    6.     int _currentHitPoints;
    7.  
    8.     IDamageable _damageInterface;  
    9.  
    10.     public HealthController()
    11.     {
    12.         _currentHitPoints = _initialHitPoints;
    13.     }
    14.  
    15.     public void TakeDamage(int damageTaken)
    16.     {
    17.         _currentHitPoints -= damageTaken;
    18.         if (_currentHitPoints <= 0)
    19.         {
    20.             _damageInterface.Kill();
    21.         }
    22.     }
    23.  
    24.     public void SetHitpointsController(IDamageable damageInterface)
    25.     {
    26.         _damageInterface = damageInterface;
    27.     }
    28. }
    29.  
     
  2. jister

    jister

    Joined:
    Oct 9, 2009
    Posts:
    1,749
    use [System.Serializable] just above your class declaration. then Health controller becomes accessible in the inspector?
    if that's what you want... don't really know what you mean with "AssertionComponent"?
     
  3. IDoAllTheWork

    IDoAllTheWork

    Joined:
    Jun 20, 2015
    Posts:
    19
    The AssertionComponent that comes with the Unity Test Tools.

    And the question is how people (if they do) use the Humble Object Pattern to separate logic from engine related stuff but still be able to run integration tests using the Unity Test Tools. Because only the HealthLogic can not be attached to a game object, you cannot access its member variables for testing purposes. If e.g. I want to test a specific situation where the game object has not the starting health, it is difficult to arrange the test.
     
  4. jister

    jister

    Joined:
    Oct 9, 2009
    Posts:
    1,749
    ah well then i was correct. serializing your non monobehaviour class will give you access to its members in the inspector.
     
  5. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    971
    A question: how will you use your code as provided to create components with a different number of hit points? As it is written, there is no way to set that value.

    Another question: are you creating a game in which no other components need to know the health of a unit aside from themselves? For instance, are you not going to display the health of units to the player? If you answer no, then I believe your behavior and your controller will need public accessors, at which point the integration test should be easy again.
     
  6. IDoAllTheWork

    IDoAllTheWork

    Joined:
    Jun 20, 2015
    Posts:
    19
    But I can only attach MonoBehaviours to game objects, can't I? At least I get error when I try to do this.

    @eisenpony:
    This is not a "real" game, but just a very basic project to get familiar with the Unity Test Tools. It's a tank that can be moved and that can fire grenades at a target, nothing more (I tried to keep it simple to be able to focus on the test tools). And yeah, you're asking really good questions. I'm still trying to figure out how to use the "Humble Object Pattern" to make my code tidied up and testable, but so far it only seems to complicate things for me (only thing that works are the unit tests).

    That's a good point. I thought it would be better to keep it hidden away, as the HealthController was supposed to handle everything related to health on its own. But yeah, sometimes it will probably be neccessary to let other objects access the internal properites.

    How do you structure your code? Do you put Unity related stuff into one class and all the logic into another class? (that would probably help with unit tests I guess) Or do you handle it completely differently?
     
  7. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    971
    This seems perfectly fine, especially if you are really keen on unit testing. However, every abstraction comes with a price, typically complexity.

    This is the same dichotomy you see everywhere in life. Agile methods and quick releases are great for projects that demand a lot of novelty: games, apps, (maybe) even operating systems... You wouldn't use the same techniques for writing a nuclear power plant's control software. Same with construction projects, you use a different amount of planning and engineering techniques when creating a tree house in your yard compared to building a skyscraper in Dubai.

    Frankly, for most of the stuff I'd be doing in Unity, I wouldn't work too hard at making my code unit-testable. Especially since it means a lot of humble object patterns; the simplest form of the delegate design pattern. I'm not anti-HOP, but you should recognize that it adds one more level of abstraction away from your actually interesting code.

    The general rule is this: put some parameters around the scope of your project and then consider what techniques you are going to apply. If you're like me, and at least 50% of the reason for doing anything in Unity is just to learn new techniques, then by all means, try using the HOP to facilitate unit testing. In that case, I think the problem you are seeing in your OP is simply a matter of making something too simple.

    This is another common theme in real life. I find a lot of forum posts and, indeed, my own experiments end up in a confused state because we are creating artificial boundaries to run tests, or to simplify things for our forum explanations. In your case, I'd say you have created the artificial requirement that the health parameter is not accessible to other objects. It's possible to create a game that has this requirement, it's just unlikely.

    So here is the crux of the problem in my mind: On the one hand, you want to keep this example simple, so you do not allow read access to the health variable. On the other hand, you want your integration tests to be robust, so you need to access the health variable. Ultimately, these requirements are incompatible, as they should be. In programming, tests are about the interfaces and APIs you create. Don't try to write a test that touches the internals of a class; that would be breaking encapsulation.

    In other words, you're doing it right! I think the only trip up was trying to make your tests more exhaustive than they ought to be given your simple example.
     
    Last edited: Nov 24, 2015
    Genghis42, melogsy and Fajlworks like this.
  8. jister

    jister

    Joined:
    Oct 9, 2009
    Posts:
    1,749
    still if you want inspector acces to _healthController in HealthBehaviour you should
    Code (CSharp):
    1. [System.Serializable]
    2. public class HealthController {
    3.     public readonly int _initialHitPoints = 100;
    4.     public int CurrentHitPoints {get{return _currentHitPoints;}}
    5.     int _currentHitPoints;
    6.     IDamageable _damageInterface;
    7.     public HealthController()
    8.     {
    9.         _currentHitPoints = _initialHitPoints;
    10.     }
    11.     public void TakeDamage(int damageTaken)
    12.     {
    13.         _currentHitPoints -= damageTaken;
    14.         if (_currentHitPoints <= 0)
    15.         {
    16.             _damageInterface.Kill();
    17.         }
    18.     }
    19.     public void SetHitpointsController(IDamageable damageInterface)
    20.     {
    21.         _damageInterface = damageInterface;
    22.     }
    23. }
    I use this a lot just to keep things as much object oriented as possible.
    actual monobehaviour classes are mostly managers or all sorts of input and collision detection needed to feed to the serialized classes.
    it's a nice way to easily integrate most know programming algorithms and systems like factories, state machines and what not...
    most of the time i even prefer having a serialized class that holds a reference to a gameobject in game instead of getting the needed code or script the otherway around, from the gameobject to a script reference like you do with GetComponent.
    I thought this was the most easy and logic way to build easy maintainable systems in Unity, didn't know that(and not sure if) it is the Humble Object Pattern.
    and what i would change is:
    Code (CSharp):
    1. public class HealthBehaviour : MonoBehaviour {
    2.     public HealthController _healthController = new HealthController();
    3. void OnCollisionEnter(Collision collision)
    4.     {
    5.         _healthController.TakeDamage(collision. relativeVelocity);
    6.     }
    7. }
    and
    Code (CSharp):
    1. [System.Serializable]
    2. public class HealthController: IDamageable {
    3.     public readonly int _initialHitPoints = 100;
    4.     public int CurrentHitPoints {get{return _currentHitPoints;}}
    5.     public GameObject myObject; //this would fit better in a base player class, but other than that this would be a reference to the active gameobject in the scene
    6.     int _currentHitPoints;
    7.  
    8.     public HealthController()
    9.     {
    10.         _currentHitPoints = _initialHitPoints;
    11.     }
    12.     public void TakeDamage(int damageTaken)
    13.     {
    14.         _currentHitPoints -= damageTaken;
    15.         if (_currentHitPoints <= 0)
    16.         {
    17.             Kill();
    18.         }
    19.     }
    20.     //same here kill i would keep out and put in my base class for the player
    21.     public void Kill()
    22.     {
    23.         GameObject.Destroy(myObject);
    24.     }
    25. }
     
    Last edited: Nov 23, 2015
  9. IDoAllTheWork

    IDoAllTheWork

    Joined:
    Jun 20, 2015
    Posts:
    19
    @jister Thanks for the confirmation, I was just to blind to see that the HealthController was actually shown inside the HealthBehaviour... ;) And regarding your recommended changes: I wanted to keep all GameObject related stuff out of the controller to keep it unit-testable, but your solution still seems to be testable. Also, I wanted to reduce OnCollisionEnter calls, so it is only done in the weapon class (which needs it anyway, as the tank grenades need to be destroyed on collision). If the weapon finds an IDamageable in colliding object, it will call TakeDamage, thus reducing OnCollisionEnter calls from 2 to 1 (unless I'm overlooking something).
    I'll have to examine it a little more to be able to decide which way to go. :)

    @eisenpony Yeah, you're right, those are very good points. Considering my goals, #1 is creating games I would like to play myself. :) Concerning coding, I just want to learn the neccessary skills to make sure my games are easily maintainable (which also means not to complicated in its coding structure), not bug infested and maybe not that hard to extend / modify. Maybe trying to push the use of the HOP to the extreme might be a little too much, considering I'm not trying to make the next Skyrim. But I guess I'll have to (and hopefully will) figure that out during my next experiments.