Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unit test projects can't reference Maui multi target projects #3552

Closed
JohnHDev opened this issue Nov 25, 2021 · 55 comments
Closed

Unit test projects can't reference Maui multi target projects #3552

JohnHDev opened this issue Nov 25, 2021 · 55 comments
Assignees
Labels
area-testing Unit tests, device tests partner/cat 😻 this is an issue that impacts one of our partners or a customer our advisory team is engaged with s/verified Verified / Reproducible Issue ready for Engineering Triage t/enhancement ☀️ New feature or request
Milestone

Comments

@JohnHDev
Copy link

Description

With the new design for multi target projects, we need to be able to unit test ViewModels stored in Maui projects.

We could move ViewModels into pure .net 6 projects, but dependencies that are defined in other multi target projects with platform specific implementations can't be mocked regardless without also moving the interfaces for those dependencies into .net 6 projects. That largely negates the positives of multi target projects if we have to create other projects just to contain the Interface definitions and view models.

Steps to Reproduce

Create a .NET Maui solution
Create a ViewModel to the .NET Maui project.
Create a unit test project and add a reference to the .NET Maui project.

Version with bug

Preview 10 (current)

Last version that worked well

Unknown/Other

Affected platforms

I was not able test on other platforms

Affected platform versions

All

Did you find any workaround?

Create another .net 6 project for view modals and interfaces that the tests and .net maui projects can reference.
That just doesn't scale well.

Relevant log output

No response

@JohnHDev JohnHDev added the t/bug Something isn't working label Nov 25, 2021
@devmikew
Copy link

Just some thoughts!

In the pure MVVM world your view models should not have references to UI components. This is so they can be unit tested!!

Which is your problem here. So what other references do you have?

Also it is possible to create another testing project which just has file links to the view models. And then run your unit tests against this.

You could also exclude other references by #if in the files and setting a TEST define in the unit test project.

@JohnHDev
Copy link
Author

@devmikew I agree, view models should not have references to UI components. Mine don't, they just happen to live in the same project as the views. They don't have to, as I mentioned. But I like to keep views and models organised.

Then consider platform specific implementations for any dependencies that view models might need, lets say a ICalendarRepository with a concrete implementation for each platform. Lets add a Calendars Maui project with the actual platform specific code, the interface definition can also be in the same project. You can't then mock that interface in a test because I can't reference the Maui project from a test project. I can create a separate project just for the interface, but that feels like massive overkill.

@devmikew
Copy link

The ICalendarRepository seems to be a data/business layer.

Couldn't you add a net6.0 target framework for this (the repository) project and in that target framework implementation do nothing. Normally you would only target android/ios/windows in this project as these are the real runtime platforms.

@JohnHDev
Copy link
Author

Not sure I follow your suggestion, concrete implementations of ICalendarRepository would be to access iOS EKEvents, Android calendar, Windows Appointments etc in a generic way. It isn't business logic, but it is native data access.

The platform specific implementation of ICalendarRepository is DI'ed into the VM constructor. The logic is held in the VM. The VM needs to be unit tested and I would need to mock ICalendarRepository (and any other dependencies it has). I can't though as the unit test project can't access the multi target library Maui projects.

As I said, I could move the interfaces into a .net 6 project, I just don't think I should have to.

@devmikew
Copy link

devmikew commented Nov 25, 2021 via email

@JohnHDev
Copy link
Author

I am talking about mocking ICalendarRepository in a test, it has nothing to do with concrete implementations.
The issue is the Maui project cannot be referenced from the unit test project. Adding a .net 6 implementation doesn't help.

@VladislavAntonyuk
Copy link
Contributor

@JohnHDev
Copy link
Author

JohnHDev commented Nov 26, 2021

@VladislavAntonyuk thanks, I have updated my project to match (including removing nunit and adding xunit) but Im not seeing any difference. What is the magic ingredient with that?

@VladislavAntonyuk
Copy link
Contributor

could you share your simple project to reproduce the issue? most likely you have some dependencies which cannot be resolved

@JohnHDev
Copy link
Author

UnitTestExample.zip

Here is a very simple example, new Maui solution, added a MainPageModel, added a unit test project, added reference to the maui project in the unit test project, it is not compatible. The unit test project would contain tests for the MainPageModel but obviously can't get that far.

@VladislavAntonyuk
Copy link
Contributor

VladislavAntonyuk commented Nov 26, 2021

I see, you try to add executable project to unit test lib. you can only add Maui Library project.
image

@JohnHDev
Copy link
Author

Its a Maui project that also generates the apps, so it really should not make any difference.

Ok, so try this one:
UnitTestExample.zip

I have moved the MainPageModel into its own .net 6 project, and added a reference to an interface held in a new Maui project. It not build because .net 6 projects can't reference .net maui projects.

As I said, I can fix that by moving the ICalendarRepository into a separate .net 6 project, but we shouldn't have to.

@VladislavAntonyuk
Copy link
Contributor

you are doing it wrong. you are trying to add maui library to net6.0 library. it is not correct.
the path should be:
net6.0=>maui library=>maui executable

@VladislavAntonyuk
Copy link
Contributor

UnitTestExample.zip

@JohnHDev
Copy link
Author

That is exactly what I said I could do, but shouldn't have to.
The solution you provided is to have another project containing all the unrelated interfaces in 1. I said right at the top that that isn't ideal, and is a poor design.

What if I wanted to create a nuget for a Maui project? It would need 2 project files, 1 for the implementation, and 1 just for the interfaces. That is certainly possible, but imo poor design.

This is the point I have raised in creating this card.

@VladislavAntonyuk
Copy link
Contributor

VladislavAntonyuk commented Nov 27, 2021

You can change your test project to support the same frameworks as your Maui project. In that case it should work.
I suppose it is by design if .NET, not only the MAUI

@devmikew
Copy link

I have a sample working with the maui project targeting net6.0. This allows the test project to reference the maui project and the repository project (assuming its net6.0). The maui project also references the repository project. You can run tests on the repository project either directly or through the maui project.

However there are some problems:

. i had to do a lot of fiddling to get the maui project to compile on all frameworks.

. I think most of the problems were to do with the project system/build system. Eg, I had to continually close and re-open VS or delete the project.assets.json and restore.

. the maui project just degrades to the view models and whatever helpers are needed.

. so i wondered was it worth it, eg, why not have a vm assembly?

.a zip is attached if your interested.

MauiTest.zip

@devmikew
Copy link

Attached is a sample project and tests where the VMs and helpers are linked into another project.
MauiAppLinked.zip

@JohnHDev
Copy link
Author

@devmikew thank you for the sample, but I don't see how that would work, you don't have platform specific implementations for the repository, which means it isn't using MAUI at all other than for the UI.

@realZhangChi
Copy link

Maybe you should use device test instead of unit test.
See this repository .

@devmikew
Copy link

@JohnHDev.

The projects I sent only provide the scaffolding for a possible implementation. Eg:

  1. The repository project has different packages references for windows and android. You could also provide one for net6.0 testing.
  2. Also the repsoitory.cs as a partial method which has to be provided for each platform.
  3. For testing purposes, it is how you provide stubs for testing the repository.
  4. The vmonly project shows how to do a testing project where you link to the VMs.

@JohnHDev
Copy link
Author

JohnHDev commented Dec 1, 2021

@devmikew thanks, I appreciate your efforts!
When multi platform projects were first suggested for Maui, this was a scenario that was brought up and discussed. Unit testing of VMs should be as simple as mocking the dependencies, calling the VM constructor, and testing. We shouldn't have to jump through hoops for this.
At the moment I have worked around it by having any common code in net 6 projects. So for example, if I have a Maui.Calendars project with the platform implementations, I also have a Calendars.Common project with the interfaces. Calendars.Common doesn't have any references to Maui, and so can easily be mocked.

Imo, .net 6 projects should be able to reference .net maui projects, in so far as being able to only reference anything in those projects that are not in the platforms folder. This is a much cleaner and simpler design and makes mocking and unit testing a breeze.

@devmikew
Copy link

devmikew commented Dec 2, 2021

@JohnHDev
I'm not sure what else to do re unit testing a maui app.

It is obvious that it can be done, 'like elephants mating'.

I think the only easy, and testable way, is for Maui to target net6.0. I imagine this would be non-trivial. Eg, it would require a lot of refractoring and split up of the maui assembly into different components.

Maybe that is not a bad thing, as how many developers in the future are going to be facing the same struggles.

@VladislavAntonyuk
Copy link
Contributor

@devmikew Maui can't target net6.0. It doesn't make any sense. Net6.0 is like netstandard (like abstract class), which only describes what you can do in all platforms. For specific implementation you target specific framework like met6.0-Android, which gives you access to Android api.
I suppose only UITest project can reference Maui.

@devmikew
Copy link

devmikew commented Dec 3, 2021

After a little inspection, maui does target net6.0. It seems to have most of the code compiled, however there is a broken reference to Microsoft. Maui Graphics.

So I'm going to dog a bit deeper over the weekend.

@trewise
Copy link

trewise commented Feb 15, 2022

Hey @devmikew, did you resolve a more straightforward solution to performing unit test on a MAUI project?

@PineYi
Copy link

PineYi commented Feb 28, 2022

Verified Repro with Android 11. Repro Project is available:
#3552.zip

@kristinx0211 kristinx0211 added the s/verified Verified / Reproducible Issue ready for Engineering Triage label Mar 1, 2022
@ismasanchez
Copy link

Is there any plan so it is possible to reference Maui multi target projects for Unit testing? The different solutions mentioned previously are just a workaround in my opinion, it is true that is good practice to separate business logic in another library, but MAUI should be testable as a single project.

@deividt
Copy link

deividt commented Jul 1, 2022

@Hottemax

I was able to successfully run xUnit tests against MAUI project adding a compiler constant for net6 target and adding a missing fake entry point for net6 only. Like so:

In the MAUI app .csproj:

  • Add net6.0 to the target frameworks:
<TargetFrameworks>net6.0;net6.0-android;net6.0-ios;net6.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net6.0-windows10.0.19041.0</TargetFrameworks>
  • Define a constant that will work only for the .net6 target:
<PropertyGroup Condition="$(TargetFramework) == 'net6.0'">
      <DefineConstants>$(DefineConstants);NET6_TARGET</DefineConstants>
</PropertyGroup>

If you try to build the app, will raise an error:

  • Program 'MauiApp1.dll' does not contain a static 'Main' method suitable for an entry point.

So, to fix that, In the MAUI app App.xaml.cs, Add a fake entry point :

#if NET6_TARGET
public static void Main(string[] args) {}
#endif

Now, the MAUI app is building and you can reference it in the xUnit test project.

The only requirements for the unit test project are:

<TargetFramework>net6.0</TargetFramework>
<UseMaui>true</UseMaui>

Keep in mind that I only tested this in a new blank MAUI app, so, I'm not sure about other blockers that may have.

@breenbob
Copy link
Contributor

breenbob commented Jul 1, 2022

@deividt I tried your approach just now in a project with ~300 unit tests, many of which are testing view models and controls from a Maui app multitarget project, and it worked great! Some tests are even passing 😝 I initially had an issue running the Maui app project on Windows after adding net6.0 as a target framework, even though it would build fine:
The recipe file "myapp.build.appxrecipe" does not exist. You may need to build your project

Building would not resolve. Curious to know if your Maui app project still deploys OK, even after uninstalling and doing a clean/rebuild?

It looked to me like it was trying to launch the app from the bin/debug/net6.0 folder instead of the net6.0-windows10.0.19041.0 folder.

So I moved the net6.0 target framework to be last, after mac catalyst, reloaded the csproj, and it seemed to kick into gear and deployed the WinUI app successfully. Thanks for sharing this!

@deividt
Copy link

deividt commented Jul 4, 2022

@breenbob, I'm happy to know that this approach worked. 😄

Though I just did some tests and I don't have real apps running, even didn't test in windows, so I don't have enough information to give you about the deployment.

Now that you mentioned about the order of the target framework. I've also noticed something similar, if I put it in different places (e.g. after the definition of OutputType) , won't build or will give different errors.

Another approach that I was trying, was just adding the net6 target conditionally, but I didn't have success with that.
Something like:

<TargetFrameworks Condition="$(OutputType) == 'Library'">$(TargetFrameworks);net6.0</TargetFrameworks>

@jfversluis
Copy link
Member

Hey everyone, thanks for all the input! We definitely need to make this story better. A while back I got this question from someone and I came up with this: https://github.com/jfversluis/MauiUnitTestSample the changes needed are in the 2 csproj files and marked with a comment that starts with xUnit. I think it's 3 places in total. Does that help at all?

I think it's pretty similar to what @deividt has been doing

@deividt
Copy link

deividt commented Jul 16, 2022

Hey @jfversluis, your approach worked in my project too. Thanks for sharing.
Now I see why my other approach didn't work. I was trying to add .net6 target conditionally, instead of adding the OutputType.

@ericnev
Copy link

ericnev commented Jul 27, 2022

This may or may not be related, but after applying the changes as described by @jfversluis , and firing up my (NUnit) tests, it barks at calls to FileSystem with a Microsoft.Maui.ApplicationModel.NotImplementedInReferenceAssemblyException. I'd have thought adding useMaui tags to the csproj would do the job, but not so much.

@Axemasta
Copy link
Contributor

Axemasta commented Jul 27, 2022

This may or may not be related, but after applying the changes as described by @jfversluis , and firing up my (NUnit) tests, it barks at calls to FileSystem with a Microsoft.Maui.ApplicationModel.NotImplementedInReferenceAssemblyException. I'd have thought adding useMaui tags to the csproj would do the job, but not so much.

@ericnev This is expected behaviour since your unit tests are targetting net6.0 instead of your platforms which target net6.0-ios, net6.0-android etc. There is no 'shared' implementations of the essentials api's since by nature they are a way of performing native / platform from shared code.

If you read the source code for FileSystem you will see on ios some lovely implementations and in the .netstandard some exceptions being thrown, since there is nothing to implement.

You will need to use the interfaces for the FileSystem (IFileSystem) and inject it into the class you want to test in order to be able to mock out the interface with something like Moq.

This approach was what we used in Xamarin with the Xamarin.Essentials.Interfaces package that would allow us to properly test our classes that needed to reference Xamarin.Essentials apis 😄

@ericnev
Copy link

ericnev commented Jul 27, 2022

This may or may not be related, but after applying the changes as described by @jfversluis , and firing up my (NUnit) tests, it barks at calls to FileSystem with a Microsoft.Maui.ApplicationModel.NotImplementedInReferenceAssemblyException. I'd have thought adding useMaui tags to the csproj would do the job, but not so much.

@ericnev This is expected behaviour since your unit tests are targetting net6.0 instead of your platforms which target net6.0-ios, net6.0-android etc. There is no 'shared' implementations of the essentials api's since by nature they are a way of performing native / platform from shared code.

If you read the source code for FileSystem you will see on ios some lovely implementations and in the .netstandard some exceptions being thrown, since there is nothing to implement.

You will need to use the interfaces for the FileSystem (IFileSystem) and inject it into the class you want to test in order to be able to mock out the interface with something like Moq.

This approach was what we used in Xamarin with the Xamarin.Essentials.Interfaces package that would allow us to properly test our classes that needed to reference Xamarin.Essentials apis 😄

I can move this discussion to somewhere else if it's not the appropriate venue, but.. there's a bit of a catch-22 here. If I target net6.0, I get that exception, as I (somewhat) expected, but if I target, for example, net6.0-windows10.0.19041.0, the tests can't find the framework, with an exception similar to the one found in #3017

I should be clear that I'd prefer to run the tests using net6.0-windows10.0.19041.0, and not mock out FileSystem.

Microsoft.VisualStudio.TestPlatform.ObjectModel.TestPlatformException: Testhost process exited with error: You must install or update .NET to run this application.
App: C:\Users\eneville\source\repos\helios-host-application\Convergent.App.Services.Tests\bin\Debug\net6.0-windows10.0.19041.0\testhost.exe
Architecture: x64
Framework: 'Microsoft.Maui.Core', version '**FromWorkload**' (x64)
.NET location: C:\Program Files\dotnet

@mikeparker104 mikeparker104 added the partner/cat 😻 this is an issue that impacts one of our partners or a customer our advisory team is engaged with label Oct 14, 2022
@aritchie
Copy link
Contributor

For those looking for a way to unit test their viewmodels by referencing a .NET6 version of the MAUI single project, here is a few csproj tricks to help you out. It isn't perfect, but will get you most of the way

https://gist.github.com/aritchie/fa5be94f831f0fe9aaefd22b029915d7

@rogersm-uwosh
Copy link

Am I reading this right? It's not possible, through Visual Studio directly, to create unit tests for .NET MAUI projects? That we need to dive into the .csproj to make things work (or just extract the model classes and put them in a separate, console-based (say) application)?

@codingL3gend
Copy link

i wrote a blog post about this here https://codingistherapeutic.com/2022/10/19/unit-testing-a-net-maui-app-with-platform-specific-code/.

Also i did recently see with a new .net maui application that the Unit Testing project doesnt have the true and it built and ran test without an issue, even referencing ViewModels. so there could be some potential there

@ismasanchez
Copy link

Again, those are all workarounds in order to solve a design fault. If you add net6.0 in the target frameworks, whenever you start using implementation specific code in your project, you enter into the nightmare realm of #if #else compilator hell. No plans to address this issue with the launch of net7.0?

@aritchie
Copy link
Contributor

aritchie commented Nov 3, 2022

@ismasanchez I don't see this as a design fault at all. MAUI gives you the standard interfaces on net60, not just the platforms. The xaml fails to compile properly right now, it is easy to move that out of the with csproj. If you think that this is truly a design flaw, how would you solve this?

You really 3 options

  1. unit test with a device runner like https://github.com/shinyorg/xunit-maui

  2. Separate your standard code into a separate library. This is a better scenario for larger teams anyhow.

  3. Realizing that my post that you suggest is a workaround, is essentially what MSFT is likely to end up doing to a large degree because that is the nature of single multitargeted projects. You also can keep platform specific code out of the way with interfaces and dumping it in the platforms folder, thus saving you from if defs.

@ismasanchez
Copy link

In my opinion, there should be at least two options, number 2 that you mentioned is adequate, but is not enough, a MAUI project should be able to be referenced by libraries requiring .net6 without any workaround.
The .csproj files should not be edited manually, my proposal would be that whenever a new MAUI project is launch, a dialog screen is shown where you choose which platforms do you want to develop (with checkboxes), if you choose only Android, only android and .net6 target frameworks are included, and both the compiler and VS should disregard .net6, so it doesn't bother you with any type of error or warning even if you implement platform specific code around the project.

@codingL3gend
Copy link

my solution to this issue only required 1 #if condition in the shared platform specific code and very minimal change to the csproj. i do agree that it shouldnt be required to have to make such changes but as i mentioned before

"i did recently see with a new .net maui application that the Unit Testing project doesnt have the true and it built and ran test without an issue, even referencing ViewModels. so there could be some potential there"

@balintn22
Copy link

I just had the same problem.
My app may be bigger, so I already had my domain logic separated out.
I wanted to test my ViewModels as well
The problem was I had VMs next to pages, to make finding corresponding pieces easier. Not being able to unit test VMs when they are part of the Maui app, I said, what the heck, time to separae them out. So I created MyApp.ViewModels net6 library, referenced from MyApp Maui executable project and mirrored the folder hierarchy - again, to be able to find things that are now pushed quite far from each other (VMs and Views). Tradeoff, acceptable.
Then came the problem of Commands, you know the new way of handling button taps. AND they should be placed in your VMs to bind the UI to. Trouble is, they are declared in Microsoft.Maui.Controls - a UI reference :-( So the way to resolve this would be to get rid of the new shiny Commands and go back to event handlers (which are implemented in View code-behind). Again, possible, but annoying.
In fact, the logic in VM tap handlers is cleaner now (without any UI responsibility), but had to put some logic into the View code-behind (thus on the UI).
And then I got stuck. I have some logic in my VM that determines the color of some UI elements via binding their color to a VM property. It used to be a color hex string, but that stopped working when I migrated my app from Xamarin to Maui. And binding BackgroundColor didn't work either. The workaround was to bind the UI elements' Background porperty (a Brush) rather than their BackgroundColor. And I couldn't be bothered to spend more time on finding a workaround for this workaround, I'm giving up on splitting out and unittesting my VMs'.

@mattleibow mattleibow self-assigned this Mar 14, 2023
@mattleibow
Copy link
Member

This should now be fixed with .NET 8 preview 2. Let me test and confirm...

@mattleibow
Copy link
Member

image

@ghost ghost locked as resolved and limited conversation to collaborators Apr 13, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-testing Unit tests, device tests partner/cat 😻 this is an issue that impacts one of our partners or a customer our advisory team is engaged with s/verified Verified / Reproducible Issue ready for Engineering Triage t/enhancement ☀️ New feature or request
Projects
None yet
Development

No branches or pull requests