Is there a way to unit test top-level statements in C#?
Asked Answered
R

1

13

I was fiddling with top-level statements as the entry point for a simple console app, since the new .NET 6 template use them as a default.

Yet, as the language specification very clearly states:

Note that the names "Program" and "Main" are used only for illustrations purposes, actual names used by compiler are implementation dependent and neither the type, nor the method can be referenced by name from source code.

So, if I can't reference the implicit Program class and it's Main() method, would it be possible to write unit tests to check the execution flow of the top-level statements themselves? If so, how?

Rabbet answered 9/1, 2022 at 20:33 Comment(5)
Is there a real need for this? Is this perhaps just a research question? Sort of like, "can I make a pig fly" type of question? (the answer is yes for pigs by the way, just not for long)Bilbao
one question: why would anyone actually want to do this? no program that's complex enough to warrant unit tests has (or: should have) any major functionality in the Main(), or on the top-level.Furlong
@Lasse A bit of both, actually. I think it's great for new learners to use console apps to improve their skills, and if they happen to be learning about unit tests it may be helpful to be able to test the execution of the compiler generated Main() method. But I have to admit that the urge to know if it's possible is mostly out of curiosity about the limitations of top-level statements.Rabbet
@Franz I agree, but I'm still curious to know if there's a way.Rabbet
Small, simple projects can also benefit from unit testing. For example even a little 10-line algorithm to "clean up" a CSV text file can apply to a lot more scenarios than you initially expect, so unit tests are a good way to be sure you're getting the new scenes right and not breaking old ones as you make changes.Lev
P
23

Yes. One option (since .NET 6) is to make the tested project's internals visible to the test project for example by adding next property to csproj:

<ItemGroup>
  <InternalsVisibleTo Include ="YourTestProjectName"/>
</ItemGroup>

And then the Program class generated for top-level statement should be visible to the test project and you can run it next way:

var entryPoint = typeof(Program).Assembly.EntryPoint!;
entryPoint.Invoke(null, new object[] { Array.Empty<string>() }); 

Something like this is used internally to perform integration tests for ASP.NET Core 6 with minimal hosting model.

Note that generated Main method can return task if you are using await's in your top-level statement, so you possibly will need to capture the return of entryPoint.Invoke and test if it is a Task and await it.

Another approach is to explicitly declare Program class as partial (for example at the end of top-level statement and use it in testing project):

// ...
// your top-level statements

public partial class Program { }
Pyoid answered 9/1, 2022 at 20:52 Comment(10)
Thanks for the detailed answer!Rabbet
Although the name Program is used right now, since the documentation explicitly states it is implementation-dependent, this might break in the future.Bilbao
@LasseV.Karlsen yes, though this approach is recommended by Microsoft for integration testing.Pyoid
There is a slight difference though. If you explicitly declare public partial class Program { }, then even if the type name hosting the entrypoint changes (as per the docs, it can), then there will still be a Program class in the same assembly, and you can then use that to get to that assembly in order to get the entrypoint for it, whichever type that may be located in.Bilbao
@LasseV.Karlsen those are alternatives. They suggest to do one or another .Pyoid
@GuruStron, thank you for your answer to the OP's question. I've got a clarifying question. You said, "...make the tested project's internals visible to the test project for example by adding next property to csproj". Which csproj file? The one for the top level statement project or the unit test project?Desjardins
@Desjardins yes, the one with top-level statement should describe another assembly(es) which should have access to it's internal methods.Pyoid
Running this gives me System.Reflection.TargetParameterCountException: Parameter count mismatch.Fly
@Fly can you please provide a minimal reproducible example?Pyoid
My Parameter count mismatch error was caused by a misunderstanding of the second parameter to entryPoint.Invoke: I thought it directly passed that parameter (the object list) to the invoked function, but it instead passes each object in the object list (in this case just the empty string array).Fly

© 2022 - 2024 — McMap. All rights reserved.