Skip to content

Conversation

@ganondev
Copy link

@ganondev ganondev commented Apr 13, 2025

Quick and dirty proof of running tests written in C#. The approach to solving the problem of "inheriting" from the base GutTest class is to create a C# version which is in theory indestinguishable from the GdScript version as an interface but in reality proxies any relevant lifecycle method calls, except those that are meant to be overridden, to an inner private instance of the GdScript GutTest node. There is also some additional utlitity around collecting and running the C# tests.

Currently this has been tested by running from the cli. This is still quite early, certainly a lot of edges to cover, but at least it "works" insofar as the C# tests are executed and passing (with warnings about orphans and such). The GutTest.cs exposes the bare minimum required to achieve that.

Given a "keep going" signal, this is what I see still needs to be done:

  • Get rid of my debugging leftovers
  • Add other assert fixtures e.g. signals
  • Copies of existing meta-tests for the C# layer
  • C# naming convention "support"
    • .gutconfig support multiple prefixes/suffixes?
    • PascalCase file and method names
    • By the way if the class name doesn't match the file name godot freaks out
  • Gut should only try to do any of this if the engine is a mono build
  • Implement some manner of doubles (mocks)
  • inst_to_dict workaround

One other potential way to look at this could reflect the engine's evolving philosophy regarding language support. The C# thing could somehow be a separate extension that gets identified and integrated as a middleware of sorts, "adding" all of these C# layers. This would set a precedent for how Gut could be adapted to support some of the other more mainstream language extensions like python. Of course this PR doesn't attempt aything like that, rather exemplifying the path of least resistance toward C# tests. Just food for thought.

@bitwes
Copy link
Owner

bitwes commented Apr 16, 2025

Very cool. It's great to see some of the work done to get an idea of what it will take. You got a lot of the basics proven out. I think the next thing to try out is creating doubles in C#. If that can be figured out then we might be on to something.

Can you comment on #576 with any reasons you have for wanting to use GUT and C# instead of using some other C# testing framework?

@ganondev
Copy link
Author

Regarding doubles, I've explored a few approaches, and based on what I can tell:

  • Doubling as it currently functions (applied to gdscript, native, and scenes) will continue to work normally. Leveraging this in C# tests works out of the box, it just means you have to manually Call doubler methods which might be tedious without a wrapper.
  • Regardless of the language used for tests themselves, doubling C# scripts in particular would be complicated. This is because doubling scripts, whether native or gdscript, requires runtime-generated subclasses of the target. It's not possible to extend C# in GDScript, so the options would be:
    • For C# specifically, generate wrappers instead of subclasses. I'm not sure of the functionality impacts of this approach. This could in theory just be a new strategy in the DOUBLE_STRATEGY enum, which is forced for C# scripts like INCLUDE_NATIVE is for native nodes.
    • For C# scripts, generate and dynamically proxy method calls using something like DynamicProxy (https://github.com/castleproject/Core/blob/master/docs/dynamicproxy.md). Unlike the GDScript generation, this would happen in-memory, managed by the mono clr. For what it's worth, this is apparently the approach that all C# mocking libraries use. Taking this a step further, a C# double could just wrap one of these libraries.

Either way, it's a decent consideration for the direction you'd want to take your codebase in. Despite being the "standard" approach for C# in other places, the latter means a completely separate doubler implementation for C#, and likely some licensing considerations, whereas the former means a big refactor to how doubling works, with some fundamental limits on functionality (off the top of my head, inheritance checks wouldn't work).

Thoughts?

@ganondev
Copy link
Author

I've come to find out that passing a C# object to some methods like asserts results in the inst_to_dict failure pointed out in the original issue, so I've added it to the list. At the moment, you can't pass C# objects as args to (some/all?) asserts. Not exhaustive but just something I've run into.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants