Thoughts on Unit Testing in C#, when you have a re...
# general
w
Thoughts on Unit Testing in C#, when you have a resource that calls out to a HttpEndpoint using HttpClient? I'm currently thinking that I need "inject" something somehow so I can override the handler in the HttpClient... but I'm not sure... would love some thoughts.
I'm happy with overriding the HttpHandler, I'm just wondering the best pattern to get the HttpClient in a way that would allow the test to override it.
b
There is no requirement that your Pulumi console application starts with
Deploymeny.RunAsync<TStack>()
. You could instead do
Deployment.RunAsync(() => your work here)
and never use the
Pulumi.Stack
object. Similarly if you are using Automation API you can do
PulumiFn.Create(() => your work here)
instead of
PulumiFn.Create<TStack>()
. If you need to return stack outputs your lambda function can return a dictionary of your outputs. Using a delegate instead of a Stack object has the benefit of allowing you do some additional setup before declaring the lambda (maybe spin up a service collection or something) which can make passing in an
HttpClient
dependency much easier, thus making unit testing easier.
w
Should that stratgey work with something like this:
Copy code
var keys = await ListWebAppHostKeys.InvokeAsync(new ListWebAppHostKeysArgs
        {
            Name = functionAppName,
            ResourceGroupName = functionAppResourceGroup
        });
I'm getting a empty value exception..
actually, not that's not right... it's
Copy code
[Input] Pulumi.AzureNative.Storage.ListStorageAccountKeysArgs.AccountName is required but was not given a value (Parameter 'AccountName')
which looks like something internal in the framework
b
that is saying you are missing the required parameter
AccountName
.
w
it is my code, similar point to the above though...
Copy code
var accountKeys = Output.Tuple(args.ResourceGroupName, appStorage.Name)
                .Apply(p => ListStorageAccountKeys.InvokeAsync(new ListStorageAccountKeysArgs
                    {
                        ResourceGroupName = p.Item1,
                        AccountName = p.Item2
                    }));

            var storageConnectionString = Output.Format($"DefaultEndpointsProtocol=https;AccountName={appStorage.Name};AccountKey={accountKeys.Apply(a => a.Keys[0].Value)}");
That's for building up the connection string required to build a functionapp with the WebApp object.
b
What's your question again?
w
that error gets thrown within a Unit test... so is it possible to unit test when that kind of method is used?
b
I think I'm confused about what the question is. You provided the first code sample which was missing the required parameter, and said you are getting the "missing required parameter" exception. And then you provided a second code sample that does provide the required parameter. So, is the first sample you provided that isn't providing
AccountName
within your unit test or not?
This sample:
Copy code
var keys = await ListWebAppHostKeys.InvokeAsync(new ListWebAppHostKeysArgs
        {
            Name = functionAppName,
            ResourceGroupName = functionAppResourceGroup
        });
Is missing
AccountName
. So I would expect to see that exception
w
The first was me misunderstanding what had failed. This bit passes when I run it against Azure.. but fails in a unit test.
Copy code
var appStorage = new StorageAccount(name.Replace("-", ""), new StorageAccountArgs
            {
                ResourceGroupName = args.ResourceGroupName,
                Sku = new SkuArgs
                {
                    Name = SkuName.Standard_LRS,
                },
                Kind = Pulumi.AzureNative.Storage.Kind.StorageV2,
            });

            var accountKeys = Output.Tuple(args.ResourceGroupName, appStorage.Name)
                .Apply(p => ListStorageAccountKeys.InvokeAsync(new ListStorageAccountKeysArgs
                    {
                        ResourceGroupName = p.Item1 ?? "test",
                        AccountName = p.Item2 ?? "test"
                    }));

            var storageConnectionString = Output.Format($"DefaultEndpointsProtocol=https;AccountName={appStorage.Name};AccountKey={accountKeys.Apply(a => a.Keys[0].Value)}");
So my guess is that these kinds of methods aren't tied to the same structures, so maybe a limitation of the unit testing? I can't imagine that the Storage account keys are stored and generated interally within the framework to be returned?
b
what does your unit test look like? When you say "within a unit test" is it just the code above?
w
just going to build a small repo and push it somewhere..
that's a test that fails... The issue is that in order to create a function app, you need to create a WebApp, and a StorageAccount, then add the StorageAccount Connection string to the appSettings of the function app, meaning that you need the account keys. That's the reason this is something that's required... I'm looking for a way around it...
you need to provide an
IMock
implementation that will serve dummy outputs when those dependent resources are created
w
so would that be the CallAsync() in this case?
b
Hmmm I was thinking it would be NewResourceAsync. When a call is made to. NewResourceAsync with type
xxx:StorageAccount
and the name that you gave it, then you want to return a dictionary that has key “ConnectionString” with your dummy value
w
The Name/ResourceGroup will need doing to get past that, but the ListStorageAccountKeys is the part where it gets tricky, as that is the only way to get the StorageAccount Keys it seems... so that outbound call needs to be mocked
b
Ah ok so yea that is probably CallAsync
w
Copy code
mocks.Setup(m => m.CallAsync("azure-native:storage:listStorageAccountKeys", It.IsAny<ImmutableDictionary<string, object>>(), It.IsAny<string>()))
                .ReturnsAsync((string token, ImmutableDictionary<string, object> args, string? provider) => {
                    return new Dictionary<string, object> {
                        { "Keys", new [] { "key1", "key2" } }
                    }.ToImmutableDictionary();
                });
That's what I have... but now I need to find what kind of structure of the dictionary it's expecting...
b
that would be whatever structure the output object of your call has for that key
w
that's a nested object... making it... complicated... it shouldn't be this complicated
Mocking infrastructure is by its very nature complicated. You'll probably need to play around with the responses and figure out what works
w
yeah, found that (and the underlying response from Azure API too.
the structure is... not pleasant to work with...
b
Copy code
new Dictionary<string, object>()
{
    ["Keys"] = new [] {
        new Dictionary<string, string>()
        {
            ["KeyName"] = "xxx",
            ["Permissions"] = "xxx",
            ["Value"] = "xxx"
        }
    }
};
does that not work?
w
Copy code
mocks.Setup(m => m.CallAsync("azure-native:storage:listStorageAccountKeys", It.IsAny<ImmutableDictionary<string, object>>(), It.IsAny<string>()))
                .ReturnsAsync((string token, ImmutableDictionary<string, object> args, string? provider) =>
                {
                    return new Dictionary<string, object> {
                        { "keys", new List<object> {
                             new Dictionary<string, object> { 
                                 { "keyName", "key1" },
                                 { "value", "blah" }}.ToImmutableDictionary() 
                        }.ToImmutableArray() }
                    }.ToImmutableDictionary();
                });
Mine does... but it's ugly as sin
b
well I don't think you need to make everything immutable so you can clean it up that way.
w
Copy code
mocks.Setup(m => m.CallAsync("azure-native:storage:listStorageAccountKeys", It.IsAny<ImmutableDictionary<string, object>>(), It.IsAny<string>()))
                .ReturnsAsync((string token, ImmutableDictionary<string, object> args, string? provider) =>
                {
                    return new Dictionary<string, object> {
                        { 
                            "keys", new [] {
                                new Dictionary<string, object> { 
                                    { "keyName", "key1" },
                                    { "value", "blah" }
                                }
                            }
                        }
                    };
                });
that's a bit nicer
b
you also don't have to use dictionaries, you could use a pre-defined POCO
you also don't have to use Moq, lol
w
Following the examples... so....
Trying to get the happy path, then add complexity, then take away the outer bits... I've got to teach this stuff, so I need to understand it bit by bit
To be clear, I got the Super Duper happy path done, then added the bit unique to what my client is doing.
If all the OutputTypes weren't sealed, that would make mocking easier... one for @tall-librarian-49374 to consider.
t
w
That's what I'm using to inject the HttpClientFactory... but how does it help with the structure of the types?
t
Not sure how non-sealed output types would help here. You are mocking the engine at the grpc level, before the deserialization to output types happens.
You may create some helpers to produce dictionaries from anonymous objects if you want
You can’t return outputs from the mocks though
w
That's what I was thinking, it's not really possible to do it without mocking at the level I'm doing it. the idea of not sealed types was so that I we could create the type, then serialize it back into the pipeline.
Guessing the structure of the responses from the gRPC side is a little painful... I'm currently trying to mock the StackReference output...
Copy code
new Dictionary<string, object> {
                        { 
                            "Outputs", new Dictionary<string, object> { 
                                { "Gateways", new Dictionary<string, object> {
                                    {
                                        "uksouth", new Dictionary<string, object> { 
                                            { "GatewayUrl", "<https://afsgsdf/>"},
                                            { "ResourceGroup", "asdfgadfs"},
                                            { "ServiceName", "asdfgsadfgh"}
                                        }
                                    },
                                    {
                                        "ukwest", new Dictionary<string, object> { 
                                            { "GatewayUrl", "<https://afsgsdf/>"},
                                            { "ResourceGroup", "asdfgadfs"},
                                            { "ServiceName", "asdfgsadfgh"}
                                        }
                                    }
                                }
                            }
                        }
I have a hierarchial output from the shared stack that looks like
Copy code
Outputs:
      Gateways: ***
          uksouth: ***
              GatewayUrl   : "https://[REDACTED].<http://azure-api.net|azure-api.net>"
              ResourceGroup: "[REDACTED]-uksouth-main89dccb89"
              ServiceName  : "[REDACTED]-uksouth-dev"
          ***
          ukwest : ***
              GatewayUrl   : "https://[REDACTED].<http://azure-api.net|azure-api.net>"
              ResourceGroup: "[REDACTED]-main4f6c847d"
              ServiceName  : "[REDACTED]-dev"
          ***
      ***
b
To simplify this, I believe you only need to mock the outputs that you are actually using in your dependents
w
yeah, unfortunately, that's a required part of the infra (linking things to things in the other stack). I think, realistically, to make this work, you need to know the structure of the gRPC response for some of the objects?
t
gRPC structure maps 1:1 to args/response types. You can also use this schema file if you want https://github.com/pulumi/pulumi-azure-native/blob/master/provider/cmd/pulumi-resource-azure-native/schema.json
w
What about the StackReference? I think that's the only one I'm missing right now.
t
"Outputs"
should probably be
"outputs"
w
I did try that, I'll have another play tomorrow. I couldn't gather anything from the main reference.