Unit-testing your AWS Lambda functions in Go

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hey everybody it's me it's Rob we are back for another edition of service office hours today's topic is unit testing your lambda functions in go if you see up here we've got a packed agenda today so I'm gonna try to get through as much of this as I can I've pre-staged some of the code but I'm also gonna like actually write it as we go so that you can see the experience if you're joining us from the AWS channel that Eric Johnson just did thanks so much for joining we'll be using the same AWS CLI that he was using I'll go ahead and drop a link to that into the chatroom so that if you don't have it you can get it installed if you want to follow along or use it later there's a link for that then so we'll create a function and go and then we'll do a little bit of a deep dive into the anatomy of a lambda function and go how they work what they look like from a type signature perspective and then we do that so that we can understand the need for receiver functions and how they interact with dependency injection or rather enable dependency injection to support our unit tests then we'll write a quick unit test work through a couple aspects of it and then move some initialization bits around in our code from the main function to the handler so that we can see the performance kit our considerations and see like which one is better off for us and what might have you choose one over the other then we'll wrap with QA and I'll just go ahead and give away the special announcement now thanks to the AWS channel for hosting right now but I will also be on that channel for next week's office hours with a very special guest richard boyd from AWS developer tools you're not gonna want to miss that one it's gonna be an exciting show so thank you all for joining us thanks to the people who contacted us after last week's show I don't want to say your names on air because of privacy concerns but if you out yourselves in the chat I will it really means a lot when we get that feedback so please just anytime you can ask your questions we're here to answer them alright let's move on so the first thing we're gonna do I posted that link to Sam CLI and it's just a it's a framework for creating our applications on AWS lambda in our case we're going to create one we see the Sam in it here and you can do runtime or - R will do runtime go 1x all of my examples today you're gonna be and go it wouldn't make much sense to do an episode called unit testing your lambda functions and go and use a different language it's my preferred language of choice there's a lot of really good reasons to use it for writing your lambda functions it's insanely fast so you can sort of accomplish the same work in less time which is less money when you're doing lambdas it's also just it's built for the cloud so huge fan this created unit testing folder for us let's open this up in Visual Studio code and let's see what we've got going on here oh you know what helps a couple things one when I open the current directory and two when I open it on the right display so let's check this out first things first I'm gonna get this nice and big for y'all and then if you don't know about this we're gonna do some toggle screencast modes so that you can see when I do stuff like bring up the command palette or when I get all pointy clicky so again when we talk about the anatomy of a lambda function we're gonna start with this example that Sam gives us this is just like a regular go program it's got its main package your imports you can define some variables we'll get rid of all this stuff because we won't need it the meat of this is right here here's your handler function and if I close this you can see the full signature so you see your handler will take in some set some individual type in object and return some individual type out object and an error now that's one signature there are seven possible signatures for lambda functions in go I'm gonna drop that link in here for you to and those are just variations of no argument or type in or error only if you return a value and only one value that must be an error if you return to values that second argument has to be an error all right so if we let's open that link ourselves and I can do a better job showing you what we're talking about here so you can declare your handler function to take and return no arguments to return only an error etc right there's also a context object that you can pass in we're gonna gonna be using the context object today so let's switch back to code alright the other thing is we've got our main right like I showed you up top this is a main go it's package main and just like any go program it has to have a main function and you'll see right here all it does is start this Handler and then the handler if you want to review the code goes in and receives the events processes the function right so we can't modify that signature to pass in things that we want to pass in we're stuck with those seven signatures and that's why dependency injection becomes important and this is a pretty well-known pattern right so dependency injection allows us to provide all of the dependencies to the function when when we call it or when we define it and that way they're not inside so as an example if we write a function that writes something to a dynamodb table then we need a DynamoDB object and we can either instantiate that here inside our handler or we can pass it in but we know from looking at those signatures that we can't just modify those signatures the lambda package will only accept one of those seven so what are we gonna do first things first I'm gonna delete this cuz we're not gonna use these we're gonna clean this coat up a little bit we'll delete all of this you see that Sam has gone ahead and given us events and lambda here I'm going to take out all of this except for the return object right because we're still going to return the same signature so what we're looking at here is this is fronted by API gateway call comes in we get that proxy request object that's T in from those seven signature types and then we do some things and we return that proxy response object that's T out and an error from those signatures all right so what we want is some way to get a DynamoDB object inside of here now we can just instantiate one and the way you normally do that is I'm gonna try to do this from scratch don't worry I'm not gonna waste a lot of your time nope it's not its session dot new session dot must session dot new there we go see I didn't have to look yet don't worry I will check and then we can do ddb equals I'm gonna have to fix that that's not right is it dynamodb mmm session all right so I'm gonna go back to check my code to make sure I'm not giving you bad info here let's make sure check myself real quick session session dot must session that new session sorry see I was giving you bad info there we go we'll skip the options and then dynamodb dot new session yeah there it is okay so there's some errors here because we haven't finished cleaning this out yet but there's a problem with this right if we want to test this function we don't have any way to invoke it from our test function by mocking something up we want a way to pass that so we need to pull that outside of here right and there's a couple ways we can do it the way I like to do it is to pull it down here into Maine and then we'll create a dependencies object call it DEP soaps no spelling there we go and we'll pass it this and table it'll be a string write the table name so we need to define this dependency object up here it's a struct and so let's look at this line first as we do this we don't want to give it a DynamoDB object because it's not actually gonna be the DynamoDB service that's running it every service in AWS in the sdk has an interface object as well and what that is is it's an interface that's normally fulfilled by the service itself but that you can fulfill via a mock to do your own testing so in this case we will pass in we wanted a DDB of type DynamoDB i face dot dynamodb API there we go and we want a table of type string right now we'll just contain our our table name down here so I'm gonna change this so I can get rid of this unused variable and since that's just a string we'll just move all this out right all right so we've got this it's a valid implementation of that type right our type depp's it needs a DynamoDB interface all right the actual dynamodb satisfies that it needs a table name so let's just call this some table you would get this from a from an environment parameter or from parameter store or something like that from your Sam template itself right now I'm just gonna hard code this stuff in so that you can see what's happening and as is my habit I'll say say that I'm getting feedback the the keyboard hints might look distracting so I'm going to turn that off let me know in the chat window if you want it back on it should be pretty obvious what I'm doing okay great so we've made these dependencies but so what like it doesn't do anything for us we still don't pass it in to the handler to do that we need to go back and address the concept of receiver functions and go and specifically pointer receivers and I'm gonna paste a link for you in here to the tour of golang it'll take you straight to pointer receivers so receiver functions allow a type to gain methods and what happens there you they can either be value receivers or pointer receivers and if their value receivers it makes a copy of the entire don't say object but object and passes it in but it's static and can't be modified if you use a pointer receiver it can accept either a value or the pointer and you can modify the values of the object and that matters because one way of doing this that well performance test later is to pass in an empty object check if the objects empty and then instantiate it and I'll talk about why you might do that but so in this case what we want to do is something like this right we want it to be lambda dot start d dot handler so to do that up here we just define D as a pointer to our dependencies and the handler right so I'm gonna again check my code real quick just to make sure I'm not giving you any errors yeah because I always get that little pointer symbol I always put it in the wrong place but in this case it's where it needs to be so we've got this handler function now that's a value receiver on a struct of type depth which is dependencies you can put whatever you want in here and we build this dependencies object for our real function with an actual dynamodb object right and you don't even need this you can you this out down here to make it even more clear that you're passing into a real dynamodb object right so I'm looking over in the chat I don't see any questions so far I know I'm moving through this pretty quickly just trying to respect your time so I'm gonna keep pushing all right that's half of this equation so we've got our dependencies type we defined the handler as a value receiver because we're limited in the types that we can pass to it I'm sorry not a value receiver a pointer receiver and then instead of invoking a row handler we're invoking the pointer receiver right so our main function is ready to go what we need to do now is wire up a test for this function and to do this this is quite a bit of code so I'm gonna copy this one and we're gonna walk through it because I've got a mock that I've already generated and so of course for any test you need the testing package here's these packages that we need again we need dynamodb here because we're defining input and output types we need the interface because as we talked about we're gonna have a mocked struct that itself has a value receiver in this case to mock that DynamoDB put item call right so this is the same signature if you go to look at the dynamodb API in the SDK this is the exact same signature as the DynamoDB put item call but instead we've made this one receive the value receiver and it just this is just a convenient structure to return whatever we pass to it all right and so this is a pretty basic test here we just want to see that the happy path works that we don't get any errors so we call this or we instantiate this and all we're doing is returning an empty put item output object and so that won't return an error which is good which means our mocked call won't return an error and now we need to construct a dependency object this of course because these are part of the same package main we don't need to redefine depths in fact its defined already for us so if I were to put something like prefix here right that doesn't make sense because that's not actually a type or I'm sorry a a property of that type so we instantiate a dependency object with our mock instead of DynamoDB and then we make a function call to the handler again we're injecting the dependencies but we're injecting the dependencies with the mock and it's enough for us the way we've written this to just pass it an empty request there all right and if you remember from the signature we should get back an API gateway proxy response in an error we're not concerned with the response we're just throwing it away and then we want to check this error here right so let me save this and in our terminal here should be able to do this from here let's see go test - V it's gonna have to go out and get some of the dependencies but ultimately it's gonna walk through and then we get this verbal right we tested that successful request path and it passes I know this is backwards from test-driven development but we can force an error there just to prove that this test is being called and not the DynamoDB table returned that take care right so we want to get that in there and now if we run this test again we get a failure right because that's what we expected everything should have been okay and it wasn't so we see that this is happening down in the test we can also check in here and we see we don't have any tables in this account so we haven't we haven't deployed anything from Sam we haven't created a simple table we haven't done any of this stuff right so all of this is running locally as we'd expect that's exactly what we want to see so this is a let me undo this here get us back to where we want to be so this is a pattern that you can use to unit test your lambda functions right you pass in everything that you would normally call in the cloud mock it up to return the values that you want to see back and then you can test the business logic of your function based on those values now one thing to consider here is that we're creating a new session and a new dynamodb object with that and it could timeout or you know the underlying object could disappear so you may want to move this code from here up into your function itself and instantiate it every time that the function is called but that could introduce a performance penalty right it has to take some nonzero amount of time to create a new session and to create a new dynamodb object the question is are we going to gain more than we lose by doing that and so the right way to do this is to test it and we'll write both let's see we've got a comment here from PG Magee thanks for your question I like this design it's nice and simple for lambda do you think there's value in using a dependency injection framework or are they heavier than needed for lambda functions so I will answer this question in two ways one you should do it the way that your language tends to prefer it or whatever tools you're most comfortable with it's better to use a heavy weight tool and write tests than to not write tests and then the second answer to that question is in go generally one of the tenants is it's better to repeat yourself when is it it's better to do a little copying then a little copying is better than a little dependency I get there eventually right my personal preference I wouldn't introduce the complexity of a framework but again as these things go to scale or if you have custom test frameworks that are developed inside your company or that you already have a lot of tests written for either one of these is fine right because you're not running your tests on production code so I would say it kind of depends on what you're used to and what is gonna enable you to write the most most correct tests that's what I would do for me I keep my lambda functions as simple and straightforward as possible so I tend to do all this by hand thanks for the question PG Magee so we've got this let's let's take a look real quick at this structure that was created and what we want to do now is modify this function before we split out that test we need to actually write this stuff into DynamoDB because we need to actually create the client do something with it and see what kind of performance differences we're looking at right so again to try and keep this short I'm gonna go ahead and copy paste this code it uses a package from segment called KSU ID it's been getting a lot of traffic on Twitter lately I'd like to thank everybody who brought that to my attention what it does is it creates a sort of time-stamped UUID that allows you to sort you you IDs by more or less buy time right not necessarily a hundred percent accurate but definitely look it up that's uh let me see if I have a link here for you I don't I'm sorry but if you look for I can I know it because we see it in the dependency segments I Oh KSU I be definitely check that out if you need something to generate you you IDs for your dynamodb tables so if you see down here I pasted all that code but there's no change here right we're still just passing this stuff into some table and up here we've still got a really tight handler function right same signature that we had before we've defined this sample order type that's going to be our ID in our DynamoDB table we've got the same dependencies that we have before a que su ID import that we talked about so we get ourselves a new ID we create that order this section right here is just assembling our go struct into the go laying map between string and object that DynamoDB expects as input and then once you've done that you can build your input object for the function and we make the call right so this will either be our mocked dependency making that call and returning a positive response or it'll be the real table making that call and putting it in now to do this we need some table so we need an actual DynamoDB table to write to it and the place we do this is in our Sam template so Sam is a transform on cloud formation and it gives you a lot of service shortcuts right this eventually becomes real cloud formation or vanilla cloud formation if you will but it offers some things to help us get done quickly we can take this environment variable out we're not going to use it and we want another resource that we'll call my table and it's this type simple table and that serverless in the middle there lets you know that it's a service transform right this is enough to create a DynamoDB table by itself it'll go out and it'll generate the name of the table based on the stack and there you go but since we've hard-coded our variable name I'm sorry our table name let's hard-code it here as well right I think I did I copy that oh no I did not okay I think I called it some table let's check yeah some table some table and so with just three lines here we've defined a DynamoDB table with on-demand capacity with only the ID field defined in the schema because it's implicit and we've given given it a name some table so again really quick really powerful way to get this going so we're still in this same directory I'm gonna run my tests again Oh see that'll that'll get fixed here in a second that needs to get the go get but Sam will take care of that for us when we run Sam build Oh all right let me go back to my working function over here it's a properties you know what that's why they're right you can't just throw a table name there you have to put it under properties yes typos so again that's just analogous to what we have up here with the function right type properties type properties all right so Sam build or Sam validate and this is a nice convenience right so it's using modules go modules I happen to be running go 114 but as long as you're 111 or up it'll work and then it tells us what to do next to get it out there that Sam deploy guided we're gonna call this unit testing I like Ohio we don't need to confirm our changes before deploy we need to confirm the creation and we're gonna save this so in the future we can just type Sam deploy so now this will put all this up there and it'll give us an endpoint it's a it's an HTTP GET endpoint I know that that's okay we're not actually gonna pass any data to this we remember in our order we faked all that data anyway so we're just doing this mainly to see what's the set up time for DynamoDB object in a session and where we should put it for performance so this walkthrough it creates the role for us it creates in fact I know already something that I did not do which is so I can show you how to update stuff forgot to give our function access to our DynamoDB table I'm gonna go ahead and do that as soon as this finishes now why not look all right I use HTTP HTTP you can use postman curl whatever you're familiar with this is gonna give us a 500 internal server error right which is the right answer because our function is not authorized to write to that DynamoDB table and so if I take us back here real quick we see all right it created the table name some table some table that's weird I must have I'll need to check into that but we can also I'm gonna blow this up for y'all all right I know it's a little small blow that one up and then if we go to our lambda function here monitoring and cloud watch it's just going to tell us again it's not authorized to perform dynamodb to put item right access tonight exception this is not authorized to perform dynamodb put item and that's because I forgot to give it those permissions so if you were with us last week you might remember we talked about some built-in Sam roles that are another thing that you get from using Sam one of them is called the dynamo DB crud policy I'm gonna paste this in here now it goes under properties so we want to be here this dynamodb crud policy I'm gonna give you a link to these as well takes an argument well I'm gonna have to get that link to you in a minute but if you search for AWS server lists DynamoDB crud policy you'll find all of them if anybody's in the chatroom they could post that that'd be great too but it takes an argument name table name in this case we just pass it a ref to my table right and then yeah it created exactly what I told it to I just told it to create the wrong thing so here we have some table our lambda function has the ability to write to it didn't need to change our code at all we're still gonna run Sam build because what that's doing is packing up the Sam template for the transform so in practice what I actually do when I'm developing is I always run Sam build and and Sam deploy just to tie those two together just a little a little quick tip for you and while that's happening I'm gonna go see if I can't find this link for you well that's updating all right here we go yeah aw Sam policy templates built-ins we love built-ins and that's still continuing anyway because it's got to delete our old table ah right after Julian sorry thanks Julian our would appreciate you getting that in there it's gotta wait for the delete on that DynamoDB table so again in the interest of time I I want to keep moving on this this will continue to go we'll come back to it that's gonna work so what we'll do here is we'll refactor this into two separate endpoints one that's a hello main and one that's a hello handler we'll leave the initiation in main and we'll move it to the handler in hello handler and then we're gonna use a tool called artillery to run some performance tests against it so first things first we'll just copy this hello world and paste it and we'll rename that - hello handler we'll rename this to hello main in our template will change all this - hello main path hello main we're gonna leave these as gets again for the reasons we discussed earlier yes in real life this will probably be a post but we're not testing the handling of the HTTP body we're testing the setup of the DynamoDB object so handler I know I can do these all at once but I'm actually really bad at Visual Studio code so I appreciate you bearing with me we're gonna take these out and so just so that we have these API endpoints for our artillery test later hello main and hello handler let's hope I got all this right all right let's give it a once-over real quick they both have access to crud hello handler hello handler hello handler hello handler these should all be main main main main main yeah great all right so we can close these what we need to open is the hello handler and it's pretty straightforward to move this what we'll do in this case is just make this DB an empty object empty struct we pull this up to an if statement in the handler right so this way if our lambda times out or if we lose the connection to dynamodb and wind up calling a different shard or whatever it doesn't have to do that the first thing we want to check is if sorry that's my JavaScript coming back in if the DynamoDB object is new then we need to set up all this stuff right otherwise if we're thinking about it how would it not be nil that's if we're running a test right we would have already passed in the object from our test and we wouldn't need to instantiate it but so at this point either it isn't nil and we can go forward or it is no and we set it up and we can go forward anyway so now we can go back here it's updated that stack before we run that test we can hit this one time and it gives us null that's actually the response we wanted because there were no old attributes to be changed so if we go back to our dyno eebee table now we've got the proper name and we've got one item in it right that just went in there by us so I'm gonna go ahead and delete this no good reason other than I like housekeeping and now we're gonna run Sam build and Sam deploy oh yeah that's me hello world main dot goes thirty-six three I know on one of these I did something weird 936 the depth so it must be hello main line 36 oh no I know what I did okay fun it's never used because it actually falls out of scope here ft da d DB is no so again because this is a value receiver we can modify the items we're going to do this this way right because D already exists independent of us so D comes in here is declared I had created another scope a D inside that scope and that's why it was failing so all right live coding all right thankfully it worked this time when this finishes the deployment it's gonna spit out two outputs while it does that I'm gonna go ahead and create the load file for Apache and what I'm gonna do here is I have one already staged I might as well just copy it here there's no need to go through that alright so while that's running we can look at this file real quick if you haven't used artillery before I'll go ahead and drop a link in there a super powerful tool basically just hammers whatever you tell it to hammer at whatever rate you tell it to hammer it and then there's not much more to it than that so I'm gonna need to replace this this is an old one and then we'll have this will actually get from hello main first I'm not gonna run these together because it'll aggregate the statistics right so I'm just gonna do the basic run these one at a time all right and then we got our end point here it's actually slash prod / hello main replace that end point looks like I probably should have deleted the word replace first huh big fail and now it's as easy as artillery run load diamo now this is this is only gonna take 30 seconds the other one will only take 30 seconds you're gonna want to do a more scientific approach to this right what's happening in the background we can go and check the logs and we'll see that we get several cold starts and lambda launches new versions of the functions there's a lot going on right so there's a lot behind the scenes here that can cause this to vary probably what you would want to do is start with something like provision concurrency in your AWS lambda function so that they're all warmed up and starting from the same place run multiple sets over a longer period of time but for this this is just sort of off the cuff with our main we've got a p95 of 315 we wanted to see it less than 400 so that's pretty good so our 95th percentile it's 315 I'm gonna copy these and save them there so this is hello Romain and then all we have to do here is modify this - hello handler and logically if you think through this what we'd expect to see is a slower aggregate time because each time inside the event loop it has to reinitialize that section now how that actually happens is gonna be interesting because it's kind of part of the run time and if that run time already has one of those objects instantiated it's not going to be nil because we modified it so who knows which way this is gonna go let's run it and see and again this will only take 30 seconds and then that'll pretty much wrap us up for today with what I wanted to talk about so while that runs while we wait for our results today we went over how to create a SAM app using Sam in it for go we discuss the anatomy of a lambda function and go and I gave you some links for the seven valid type signatures for lambda functions we talked about receiver functions specifically pointer receiver functions and dependency injection and we took we wrote a unit test and now we're wrapping up by looking at the performance considerations of moving that initial excuse me initialization out of main and into the handler so if we copy that and compare this this is Hello Handler and we compare this side-by-side with where as hello main let me close this real quick alright so if we look at these side-by-side [Music] slightly slower minimum faster maximum which is an interesting result the medians very close p95 is very close 99th percentile it's almost a dead heat right so you can see that there's very little actual performance implications moving that initialization out of your main function and into your handler function which i think is kind of an argument for moving it up into your handler function if it's more or less equivalent from a performance perspective then by doing it in your handler every time you're guaranteed that if if something underneath changes that's outside of your control you still get that connection and that session reopened for you so based just on this little bit of testing that's how I would write my my lambda functions and it's actually how I write my lambda functions from time to time we got a pee McGee here sorry PG McGee here with another question I know lambda cold-start times have improved recently what's the current average cold start time for go fast like I can't give you the actual numbers off the top of my head for two reasons one because I don't know them that's the most important one what I do know is comparative numbers and go is the fastest runtime that we see for cold starts we see surprisingly fast run starts with I'm sorry cold starts with Python as well but we've also done a lot across the board to reduce cold starts whether it's from enhanced V PC networking that was released for your lambdas that run in a V PC whether it's provision concurrency there's a lot that we've done to reduce the implication of that so if you haven't checked in to provision concurrency today I definitely would it if you know your usage patterns and they're not super spiky then right around 60 percent utilization you actually start paying less money for having provision concurrency so not only is it faster with fewer cold starts but it's cheaper if you if you have that sort of model of usage so thanks for that question PG McGee that's it I know I've taken 45 minutes today I really appreciate everybody coming out and watching this again this time same time next week on Thursday I'm gonna be on the AWS main channel with AWS developer tools developer advocate Richard Boyd you're definitely going to want to see that one it's going to be an exciting episode where roll walk through again hands-on with some tools thanks again to everybody who joined today thank you for people who have sent me questions in Twitter via direct message we love hearing from you so whether it's a feature request or just a way to get more understanding please reach out to us at any time again on twitter i'm RTS underscore robb and my dm's are open so others uh let's see Julian would added a little bit of context to that answer as well depends on what you run in your function cold starts have two parts AWS in your code also remember cold starts only affect a very small percentage of you function starts that's true and especially depends on the pattern of your function if you have very short running functions which go functions tend to be the fastest running ones that we see then you don't need as many running concurrently to handle the same load of execution right because the worker releases one invocation and is ready to accept another invocation more quickly so if it ran for 100 milliseconds then you could turnover 10 for each instance every second compared to something that's running for 500 milliseconds which could only turn over two so you would see five times fewer cold starts at least right and then once those are warm they stay their warm running your functions for a little bit of time until they come back down and that's without provision concurrency with provision concurrency you're defining a floor of warm functions that'll always be there for you any other questions from anybody all right I'm gonna wrap it up here thanks everybody so much again we will be back next week on the AWS channel that is I'll put a link in here for you just in case you can't find it but it's not hard to find it looks just like that twitch.tv slash AWS thanks very much everybody have a good one
Info
Channel: AWS Events
Views: 8,408
Rating: undefined out of 5
Keywords: AWS, Events, AWS Lambda, AWS SAM, SAM CLI, unit-testing, coding, troubleshooting
Id: dFY2hsBiFcI
Channel Id: undefined
Length: 45min 36sec (2736 seconds)
Published: Mon Apr 06 2020
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.