firebase-admin-dotnet icon indicating copy to clipboard operation
firebase-admin-dotnet copied to clipboard

Add support for Mocking

Open ghost opened this issue 4 years ago • 5 comments

As far as I can tell, all IFirebaseServices are sealed and have internal constructors.

This presents us with the challenge of how we can test code, which uses the FirebaseAdminSdk, which contains very critical operations. I ask you to re-evaluate this design decision, as mocking is required to achieve a high test coverage of our own code, which interfaces with FirebaseAdmin SDK directly.

In my project, I was able to identify two critical firebase services, which lack mockability:

  • FirebaseAdmin.Messaging.FirebaseMessaging
  • FirebaseAdmin.Auth.FirebaseAuth

My environment:

  • .Net Core 3.1
  • NUnit 3.12.0
  • Moq 4.14.1

ghost avatar Jun 16 '20 11:06 ghost

If it is of any help, on other context, i work-arrounded this issue with some effy use of reflection, in this case for firebase Auth, im guessing that you can use the same steps for fcm

/// <summary>
/// A really weird way to instanciate what we need to test, 
/// working arround internal classes and missing constructors
/// </summary>
private static FirebaseToken AssambleAToken(string subject, string phoneNumber)
{
    // The thing we want to create
    var fireType = typeof(FirebaseToken);

    // Get Firebase Assambly
    var assembly = fireType.Assembly;

    // Find an internal type that handles the args
    var argType = assembly.GetTypes()
        .FirstOrDefault(a => a.Name == "FirebaseTokenArgs");

    // Create the instance without using any constructor
    var args = FormatterServices.GetUninitializedObject(argType);

    // Get all the properties
    var argProps = argType.GetProperties();

    // Set the subject
    argProps.First(a => a.Name == "Subject").SetValue(args, subject);

    // Set claims
    var claims = new Dictionary<string, object>
    {
        { "phone_number", phoneNumber }
    };
    argProps.First(a => a.Name == "Claims").SetValue(args, claims);

    // Get the appropiate internal ctor (im starting to hate my life)
    var ctors = fireType.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance);

    // Instanciate the final result with the weirdly formatted args, and return it
    return ctors[0].Invoke(new object[] { args }) as FirebaseToken;
}

Dongata avatar Jun 16 '20 11:06 Dongata

If it is of any help, on other context, i work-arrounded this issue with some effy use of reflection, in this case for firebase Auth, im guessing that you can use the same steps for fcm

/// <summary>
/// A really weird way to instanciate what we need to test, 
/// working arround internal classes and missing constructors
/// </summary>
private static FirebaseToken AssambleAToken(string subject, string phoneNumber)
{
    // The thing we want to create
    var fireType = typeof(FirebaseToken);

    // Get Firebase Assambly
    var assembly = fireType.Assembly;

    // Find an internal type that handles the args
    var argType = assembly.GetTypes()
        .FirstOrDefault(a => a.Name == "FirebaseTokenArgs");

    // Create the instance without using any constructor
    var args = FormatterServices.GetUninitializedObject(argType);

    // Get all the properties
    var argProps = argType.GetProperties();

    // Set the subject
    argProps.First(a => a.Name == "Subject").SetValue(args, subject);

    // Set claims
    var claims = new Dictionary<string, object>
    {
        { "phone_number", phoneNumber }
    };
    argProps.First(a => a.Name == "Claims").SetValue(args, claims);

    // Get the appropiate internal ctor (im starting to hate my life)
    var ctors = fireType.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance);

    // Instanciate the final result with the weirdly formatted args, and return it
    return ctors[0].Invoke(new object[] { args }) as FirebaseToken;
}

Thanks, this helps me create my response objects (I didnt know FormatterServices.GetUninitializedObject), but mocking the services methods in a sane way is still missing some pieces (Not too excited to write my own mocking framework for this SDK)

ghost avatar Jun 16 '20 11:06 ghost

I ended up wrapping the functionality I wanted in an IFirebaseService interface with the features I needed from firebase. I've then wrapped an implementation around the FirebaseAdmin SDK

public interface IFirebaseService
{
    Task<User> GetUserAsync(string uid);
    Task UpdateUserAsync(UpdateUserArgs args);
    Task SetRoleAsync(string uid, UserRole role);
    Task<string> GenerateEmailVerificationLinkAsync(string email);
}

floppydisken avatar Mar 16 '21 11:03 floppydisken

@Dongata Thank you for the helpful solution! One note for future readers: It looks like the FirebaseTokenArgs class was renamed to just Args, as can be seen here: https://github.com/firebase/firebase-admin-dotnet/blob/master/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs#L83

nmehlei avatar May 26 '21 21:05 nmehlei

@nmehlei You welcome man, I had to update the test again (we updated everything to net 5) here's the code updated, hope it helps.


/// <summary>
/// A really weird way to instanciate what we need to test, 
/// working arround internal classes and missing constructors
/// If you're brave enough try to understand it, i mean, it's not as bad
/// it's full of comments.
/// </summary>
private static FirebaseToken AssambleAToken(string subject, string phoneNumber)
{
    // The thing we want to create
    var fireType = typeof(FirebaseToken);

    // Get firebaseToken internal args
    var internalTypes = fireType.GetNestedTypes(BindingFlags.NonPublic);

    // Find an internal type that handles the args
    var argType = internalTypes
        .FirstOrDefault(a => a.Name == "Args");

    // Create the instance without using any constructor
    var args = FormatterServices.GetUninitializedObject(argType);

    // Get all the properties
    var argProps = argType.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance);

    // Set the subject
    argProps.First(a => a.Name == "Subject").SetValue(args, subject);

    // Set claims
    var claims = new Dictionary<string, object>
    {
        { "phone_number", phoneNumber }
    };
    argProps.First(a => a.Name == "Claims").SetValue(args, claims);

    // Get the appropiate internal ctor (im starting to hate my life)
    var ctors = fireType.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance);

    // Instanciate the final result with the weirdly formatted args, and return it
    return ctors[0].Invoke(new object[] { args }) as FirebaseToken;
}

Dongata avatar May 26 '21 22:05 Dongata