firebase / firebase-admin-dotnet

Firebase Admin .NET SDK
https://firebase.google.com/docs/admin/setup
Apache License 2.0
367 stars 131 forks source link

Add support for Mocking #196

Open ghost opened 4 years ago

ghost commented 4 years ago

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:

My environment:

Dongata commented 4 years ago

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;
}
ghost commented 4 years ago

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)

floppydisken commented 3 years ago

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);
}
nmehlei commented 3 years ago

@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

Dongata commented 3 years ago

@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;
}