Search Unity

Question How to set up code for unit tests in a practical way?

Discussion in 'Testing & Automation' started by MrKodei, Aug 22, 2023.

  1. MrKodei

    MrKodei

    Joined:
    Aug 8, 2021
    Posts:
    15
    Hey there, I got one practical question regarding unit tests:
    So in my understanding (i am pretty new to this topic), the point of unit testing is, being able to test your actual code. And even more so, being able to test it through production. So that, when the code changes, the test can spot that difference and report an error.
    So i would want to test my ACTUAL code from my ACTUAL classes. I learned, that this is not so easy, since you can not create monobehaviours in EditMode (which are, from what i heard, preferred, since they don't need as much set up and are faster). So one solution is to use NSubstitute, in which you create Interfaces of your classes.
    Now to my question:

    Interfaces can not have concrete implementations of methods. And i can not access the methods of my "normal monobehaviour class" (e.g. character), because i can not instantiate it in the test enviroment (EditMode Tests with NSubstitute).

    That means in order to access and test a concrete implementation of a method, i need a helper method (either static or from a non-monobehaviour). Would you have two methods, one in the class (main method), and on as a helper? But then you would always have to change the helper method as soon as you change the "main method", which means you could forget it and therefore have bugs, despite your test passing. Or would you use helper methods in the first place and use them in the "main class" (like a damage calculator)? I hope my question is understandable :D thanks a lot !

    CharacterClass.png CharacterInterface.png View attachment 1291356
     

    Attached Files:

    Last edited: Aug 22, 2023
  2. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,848
    You got that wrong, like so many other devs. ;)

    Unit tests exist to test code as and even before you write the actual code. You test one thing at a time, and keep updating the tests before changing or extending the production code. This practice is called TDD (test driven development). Doing so leads to clean code that continues to be testable and easier to maintain, refactor and extend. Recommended reading: Clean Code and other „Clean“ books by Robert C. Martin.

    Whereas if you write production code, and then want to add unit tests to it, you will quickly run into plenty of issues getting your tests to work in the first place due to dependencies already in place that make testing the code a chore. The tests also tend to be too complex and break often. That‘s also why many devs eventually drop writing tests. That‘s where you are at. You probably think you have to test everything together - but that‘s not a unit test, those are called integration tests.

    For example, if you want to test the player shooting an enemy and the enemy dies because of that shot, that‘s an integration test. Unit testing done properly has you write tests for the following things:

    • If player input „fire“ event occurs, the corresponding class should execute the FireWeapon() method. This could be determined with a mock class, or by testing whether a projectile has been instantiated.
    • A „enemy gets hit“ test confirms that a projectile heading towards an enemy (both spawned in the test setup) actually runs the DealDamage() method of the enemy.
    • Another test should check that if the projectile hits anything, even if it does not have a DealDamage() method, does not throw an error and destroys the projectile.
    • Yet another test confirms that if DealDamage() called on a specific enemy with a specific projectile will instantly kill (destroy) the enemy.
    • Lastly, you may have more tests to confirm that if an enemy dies, it
      • plays (instantiates) a particle effect
      • plays the „argh I‘m dying!“ audio
      • changes the enemy‘s state to „dead“
      • changes the animation state to „dying“
      • removes the enemy from the AI list of enemies (eg pathfinding etc)
      • enables the enemy‘s ragdoll physics
      • removes the corpse after a given time
    Some of that seems like overkill and it surely is, for example checking if an audio gets played. How much detail the tests need to have is up to you, but certainly as soon as you have a bug where one of these things isn‘t working right you should test it so it never happens again.

    Anyhow, you can certainly instantiate gameobjects and thus monobehaviours in edit mode. But clearly some tests need to be in playmode, for example anything timing related.
     
    swittrock likes this.
  3. MrKodei

    MrKodei

    Joined:
    Aug 8, 2021
    Posts:
    15
    Hey there! Firstly, thank you very much for your time and your elaborate answer! I feel like i do understand the concept of the "unit" in unit test better now, and thinking with the " unit" in mind, leads to writing different (more testable code). I am still a bit confused on the concrete implementation for that method within unity. Maybe i am just missing a technicality here?

    In unity, within the test runner in EditMode, i can only access mock up interfaces. That means i can never access concrete implementations of a "real" class (e.g. Character) and call on its methods for testing purposes. That means i would have to write separate code with the same logic within my testing enviroment (create a non monobehaviour).

    Now lets say the test passes and i implemented the same logic within my "real" class and my "testing" class. Now something forces me to alter my "real" code, which does NOT automatically alter my "testing" code. So i would have to go about and change that also, in order to catch any tests that now fail.
    Otherwise, if i would not change the "test code", it would still pass, but would not be a representation of the "real" code anymore.
    I would think it should be possible to test my "real" code, so that when it changes, the tests now test the new code.

    What would be your suggestion here? Doing these things in PlayMode Testing and just test it there?

    I kinda feel like a still have the wrong take on this topic and that's why it's not working out for me. I am really sorry for my noob questions. I do feel like Unit Tests are a very powerful tool and i would love being able to use them.

    Thanks!
     
  4. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,848
    What gave you this impression?
    Perhaps, given the fact that unit tests are in an assembly definition, your actual code may not be in an assembly definition and thus you can‘t reference it in tests? But then you couldn‘t use the interfaces either. Certainly it‘s not true that you can‘t access production classes in unit tests. You just have to add them as a dependency to the test‘s assembly definition.

    A great IDE would do that automatically and even allow you to run unity unit tests from within the IDE. Hint: it‘s not a Microsoft product. ;)
     
  5. MrKodei

    MrKodei

    Joined:
    Aug 8, 2021
    Posts:
    15
    My Assembly Definition can access my scripts. But its the fact that you can't create new Monobehaviours in EditMode Tests (such as my Character class). Therefore i need to use NSubstitute to create an Interface of that class, which can be substituted for in the EditMode Tests. But the problem is, these interfaces can not implement concrete implementations of the class methods, which means i can not directly access my monobehaviour code of my character class. And therefore i need to either build my class as a non-monobehaviour or have some helper classes, which implement the exact same logic as my monobehaviour character class.

    But again, i guess that i not really the point of unit tests or?
     
  6. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,848
    What makes you say that?

    This works just fine in edit mode tests:
    var character = gameObject.AddComponent<Character>();
     
  7. MrKodei

    MrKodei

    Joined:
    Aug 8, 2021
    Posts:
    15
    Well since my TestClass does not inherit from Monobehaviour, it does not know "gameObject".
    upload_2023-8-24_0-8-55.png
     
  8. MrKodei

    MrKodei

    Joined:
    Aug 8, 2021
    Posts:
    15
    Okay this works.

    upload_2023-8-24_0-22-21.png

    Well, i guess i was being stupid. Since i'm new i was watching this video:


    And i felt like, i could not directly work with my monobehaviours. So i don't really understand the purpose of NSubstitute, when the only obstacle is, creating a GameObject before adding the component.

    I guess this works just fine. I am really thankful for your time and help!
     
  9. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,848
    No problem, glad I could help. And yes, creating a GameObject is the way to go. An alternative is to load a test scene in the SetUp method and reload the previously active editor scene in Teardown. That way you can work eith objects already in the scene which is sometimes useful to avoid writing a lot of boilerplate code to instantiate objects and adding components and all that.
     
  10. MrKodei

    MrKodei

    Joined:
    Aug 8, 2021
    Posts:
    15
    That's also a very useful tip! I will definitely be playing around with that and keep learning. Thanks again!