https://pulumi.com logo
Title
w

worried-knife-31967

03/10/2021, 4:16 PM
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

bored-oyster-3147

03/10/2021, 4:30 PM
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

worried-knife-31967

03/10/2021, 5:36 PM
Should that stratgey work with something like this:
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
[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

bored-oyster-3147

03/10/2021, 5:38 PM
that is saying you are missing the required parameter
AccountName
.
w

worried-knife-31967

03/10/2021, 5:41 PM
it is my code, similar point to the above though...
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

bored-oyster-3147

03/10/2021, 5:43 PM
What's your question again?
w

worried-knife-31967

03/10/2021, 5:43 PM
that error gets thrown within a Unit test... so is it possible to unit test when that kind of method is used?
b

bored-oyster-3147

03/10/2021, 5:47 PM
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:
var keys = await ListWebAppHostKeys.InvokeAsync(new ListWebAppHostKeysArgs
        {
            Name = functionAppName,
            ResourceGroupName = functionAppResourceGroup
        });
Is missing
AccountName
. So I would expect to see that exception
w

worried-knife-31967

03/10/2021, 5:56 PM
The first was me misunderstanding what had failed. This bit passes when I run it against Azure.. but fails in a unit test.
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

bored-oyster-3147

03/10/2021, 5:59 PM
what does your unit test look like? When you say "within a unit test" is it just the code above?
w

worried-knife-31967

03/10/2021, 6:00 PM
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

worried-knife-31967

03/10/2021, 6:30 PM
so would that be the CallAsync() in this case?
b

bored-oyster-3147

03/10/2021, 6:32 PM
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

worried-knife-31967

03/10/2021, 6:34 PM
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

bored-oyster-3147

03/10/2021, 6:35 PM
Ah ok so yea that is probably CallAsync
w

worried-knife-31967

03/10/2021, 6:42 PM
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

bored-oyster-3147

03/10/2021, 6:58 PM
that would be whatever structure the output object of your call has for that key
w

worried-knife-31967

03/10/2021, 7:01 PM
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

worried-knife-31967

03/10/2021, 7:15 PM
yeah, found that (and the underlying response from Azure API too.
the structure is... not pleasant to work with...
b

bored-oyster-3147

03/10/2021, 7:25 PM
new Dictionary<string, object>()
{
    ["Keys"] = new [] {
        new Dictionary<string, string>()
        {
            ["KeyName"] = "xxx",
            ["Permissions"] = "xxx",
            ["Value"] = "xxx"
        }
    }
};
does that not work?
w

worried-knife-31967

03/10/2021, 7:27 PM
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

bored-oyster-3147

03/10/2021, 7:30 PM
well I don't think you need to make everything immutable so you can clean it up that way.
w

worried-knife-31967

03/10/2021, 7:32 PM
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

bored-oyster-3147

03/10/2021, 7:32 PM
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

worried-knife-31967

03/10/2021, 7:33 PM
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

tall-librarian-49374

03/10/2021, 7:57 PM
w

worried-knife-31967

03/10/2021, 8:02 PM
That's what I'm using to inject the HttpClientFactory... but how does it help with the structure of the types?
t

tall-librarian-49374

03/10/2021, 8:02 PM
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

worried-knife-31967

03/10/2021, 8:07 PM
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...
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
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

bored-oyster-3147

03/10/2021, 8:32 PM
To simplify this, I believe you only need to mock the outputs that you are actually using in your dependents
w

worried-knife-31967

03/10/2021, 8:35 PM
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

tall-librarian-49374

03/10/2021, 8:42 PM
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

worried-knife-31967

03/10/2021, 9:01 PM
What about the StackReference? I think that's the only one I'm missing right now.
t
"Outputs"
should probably be
"outputs"
w

worried-knife-31967

03/10/2021, 9:52 PM
I did try that, I'll have another play tomorrow. I couldn't gather anything from the main reference.