 All right. Well, hey guys. Welcome to the testing and automation dev room today. I'm going to be talking about Writing good tests writing go tests and writing good go tests probably in that order I tried to my best to cater this talk to a wide range of experience with go So if I have experts listening out there, please just be patient with some of the introductory material and I promise we'll work our way up to some of the more complex cases and And to any novices if you're new to go don't get don't get discouraged by any unfamiliar go Concepts or packages much of what I'm going to cover is like directly from the go doc So if you want to reference those at the end to revisit anything you're welcome to do so Hopefully regardless of your experience level you're all visual learners I'm going to have a lot of code on the screen and kind of be like tutorial based For each concept so hopefully by the time we're done here You guys will walk out with a better understanding of best practices in general testing and in goaling testing My name is Nikki Atea and I'm a work-from-home dog mom who hacks on distributed systems and workflows I'm based in Southern, California And you could find me at my handle listed on any social media You could just look for the baby Yoda avatar sipping a soup and that's me I Why am I talking about go tests? I have a long history with testing and go So I at the power and performance team at Apple I maintained automation frameworks for iOS and watch OS performance testing and we used Ruby and Python for that and currently I'm a software engineer at Sun Sue Which is an open source and open core monitoring tool kind of like Nagios on steroids if you're familiar with either of those And for about the last three years. I've been contributing to the sense you go Project which judging by the name is a rewrite and go Sonsu Inc the company I work for Has supported all of my efforts to come here from halfway across the world So a huge thank you to them and their support of open source So if you're curious about what we do at Sonsu at all you could check out our github right there All right, so let's start by answering the first question You might have on your mind why go Believe it or not There's a lot more to go lang than the cute little gopher mascot such as this Belgium styled gopher by Ashley McNamara For starters, it's an open source language, which is pretty important designed by Google. You could find it at github.com slash going So it's a comparatively young language especially compared to the ones that more of the popular ones we use today And it's been almost eight years since its initial release to the public so as a result the original offer authors were able to address more Current and relevant problems in the landscape Like at the time that they were designing it So these include runtime efficiency high performance networking and multi-processing So as such many consider it like the hot new language and distributed programming Including those big companies like uber twitch Netflix, which all require like high concurrent performance. So It's a good language Now that we have a little go history. Let's talk about the design of the language For the most part it's considered Statically and structurally typed it's type safe requiring a strict type for each object in field memory safe Allocating memory for each object in field. So in these instances the compiled binary is a lot faster at runtime Which of course is a performance win But the caveat to this characteristic is that go also supports some instances of dynamic typing So if you're used to like Ruby and doing really dirty like Things in Ruby go is capable of doing that But it's just more considered on the statically typed language side So using techniques such as reflection you can treat go objects generically at a little bit of a performance trade-off. So Generic functions if you decide to do that They encourage code reuse and can compile the program faster But might be a little slower at runtime because of the additional dynamic processing it's doing So for the sake of testing that will focus more on the statically typed cases because they're very straightforward It is a compiled language which with a very large standard library It produces tools such as go build go run and go test Therefore when you download the language itself, it's quite large. I think comparatively the other languages as well The most recent version of go is a hundred sixteen megabytes on Mac OS and Linux systems But due to the enriched Standard library that they have a lot of the functions you're going to need on a day-to-day basis such as IO calls OS exec encoding networking syncing and of course our testing operations Are all contained in these built-in packages. So it limits the need for external dependencies So Speaking of containment It's self-contained. So you say go build and it's good to go So when you run go build it produces a static binary Against a target operating system and architecture. So the resulting binary includes any external dependencies you have with your dependency management system and It'll run on systems that don't have the language installed. So it makes distribution of go projects super super easy and Finally, it's concurrent and asynchronous with go routines programs can handle tasks simultaneously And with channels and weight groups programs can execute tasks asynchronously So both of these properties improve performance and responsiveness for your program Regardless of the pros and cons of these characteristics as a whole they all have a different impact or implication on how you would write tests So tests like I mentioned in Structurally type languages are a lot more straightforward because your input and output types are already defined and You would probably have a compile time error if that was incorrect Therefore testing fixtures are a really common practice in go They can be read in from a JSON file or compiled in your go file directly But essentially they'll provide a quick reliable and easy way to invoke test artifacts in your tests The standard library makes writing tests really really convenient The testing package is built in and it's really huge. It's got Functions to make assertions and they can be made without any external dependencies It also has packages for mocks and things such as like a test HTTP server Which we'll get into a little later for more of them more involved integration tests Self containment how this relates to testing is it helps keep tests organized Unit and integration tests are written as go files and they live directly in your code These tests of course are not built and packaged and shipped when you run go build But you can easily run the test in a CI pipeline and the end test However capture more of a larger scope, but go build kind of empowers you to do that a little easier in a CI pipeline So you could go build and then run and end tests on that resulting binary The concurrent and asynchronous features of a go program are some of the biggest challenges to testing and go Because they're more prone to race conditions and blocking calls Therefore running these kinds of tests with a race detector and a timeout is typically encouraged So we'll get into some examples about how to deal with concurrent complexities and tests But for now, I hope I've convinced you the pros Outweigh the cons when it comes to go design and tests So whether you came in new to the language or not, I hope that background was useful Because it's gonna help us understand at greater depths You know the background of the language and help us write better tests So regardless of the language we choose even if you're not choosing go and you're just trying to get a good seat for the next session We still need to be able to recognize what actually makes a test good and what techniques we can use to write them simply and concisely So what are some properties of well written tests? Similar to a scientific experiment a good test will only test one thing at a time so control variables are designed to isolate the effects of a single independent variable on that it has on that outcome and Just as there are control variables and like a scientific experiment like this a test should also contain controls for each iteration So for each thing you're stressing You should have some constants around it Even in larger scope tests such as end-to-end testing you want to focus on Testing just the happy path or just a networking failure for example Otherwise you could end up with multiple points of failure, which makes it really difficult to isolate which component caused what? While we want to make sure we're only testing one single thing at once We also want to make sure we're testing many things together and that's not a contradiction. I promise software components can have many like entangled interdependencies So for this reason we should exercise as many permutations as possible in our tests So each permutation of code like in this image is going to be unique in context and order And although the permutations permutation itself is unique. It can still yield the same outcome as other tests so for example like having the same number of blues and reds in each column and Just because we have a redundant outcome doesn't necessarily mean that the test is bad And we're being redundant because we're actually Creating multiple success and failure scenarios Triggered by all of these different variables working together. So The variety of the code paths that get touched by these different combinations are what makes a test really really good Are you guys familiar with Marie Kondo in Europe some of us, okay So we want to be able to test a single thing at once and additionally multiple things together But how are we supposed to do that especially in systems that grow in complexity over time and often like per commit? Simply put we can Marie Kondo our code. She's a Netflix documentary person and she helps people like organize their garage and stuff It doesn't mean we want to erase the code that like doesn't spark joy like we still need our code But for example if a project Resembles a messy garage that hasn't been cleaned out in years It's going to be really difficult to verify the contents and usability of anything that's in there So on the other hand if we Marie Kondo to garage will probably have Transparent containers. They're clearly labeled organized by season or activity And they're easily accessible for when we need it. So the same goes for code in that metaphor Organization and compartmentalization will help simplify our code and therefore our tests So our unit tests are smaller the testing smaller things By dividing complex tasks and functions into smaller and more manageable pieces We can encourage some of that same code reusability and isolate those specific problem points So obviously those smaller code blocks are far easier to unit test Then large like run-on integrated code blocks that just start getting really like intertangled Another component to writing good tests is writing failing tests This tweet says you from another talk. I'm not sure where Says you should never trust the test. You haven't seen fail I'm not sure that Joe or Gwen here were the first ones to say that I don't know if they're gonna be the last ones to say that because I'm saying that now But we shouldn't just write tests to pass we should be making incorrect assumptions on our code So we can actually observe that failure So personally my favorite way of doing this in practice is with bug fixes So if I've narrowed down kind of to where I think the problem is I'll immediately write a test for it Without even attempting to like fix the bug Observe that it fails ultimately reproducing the bug that's reported And then I can Basically confirm that the code was behaving improperly before and properly once the fix is shipped So your code reviewers will thank you when your test explains exactly what's going on So that last slide is really just a funner and like longer way to say test-driven development Some might argue that it's just a vicious cycle of endless handoffs or maybe riddled with like developer bias But in many cases At least in unit case unit tests it will help you achieve really good code and really good coverage So in practice writing a test prior to fixing the bug or implementing a feature sounds great, especially for those smaller scope tests But in reality for real-world applications and larger scope tests They're not as easily testable through this method. So don't beat yourself up over it I'd recommend using a test-driven development when possible, but focusing more on shipping organize and compartmentalized code Which in the end makes it more testable So that's kind of my overview on writing good tests and best practices I write and speak a lot about testing in general So if you want more of that be sure to check out the resources at the end, but for now Let's shift into go tests specifically Before we start writing go tests, we'll kind of discuss how we would run a go test I mentioned earlier that going produces tools such as go build go run and go test and you guessed it go test is meant to run our tests So it includes other features such as benchmarking and parallelization Which we won't get into in this talk, but they're great resources Into gathering and reporting some more context about your code So to test you simply run go test from the directory where your tests are stored or you can utilize some other attributes of the tool So here's a couple use cases for how one might use the tool You can run all the tests in the current working directory You can run a single test against a fully qualified package name, which you have like the github path there You can tag go files with a simple comment at the beginning of the file So if you want to just run integration tests, you could tag all of your integration files with that and that's just going to run all of those I Think I skipped over dash v is just going to verbose the print any output any additional output From your go testing tool You can enable race detectors with dash race and timeouts for tests that might depend on blocking calls So if you're calling something with dash race, you're probably going to want to time out as well You can also get a snapshot of code coverage with dash cover I know code coverage can be a pretty controversial topic While I believe we should all be shooting for a hundred percent code cub at least on unit tests it's really not feasible in some instances and Writing high-quality code in meaningful tests is a lot more important than just checking that box and saying like okay a hundred percent code Cove we're good And lastly some editors such as VS code, which is what I use can even highlight the covered and uncovered lines of code if you run the tool through the editor So it's kind of really helpful to identify if your test is actually hitting those lines Some basic syntax and project details that you'll need to know about writing go test before we get started First is the file naming convention All shipable go code should be in a file ending in go just like that Well all non-chipped test code should have the postfix Underscore test go so it's still a go file. It just has that test When writing fixtures for your go-structs that are going to be using your tests I would actually recommend to organize those fixtures in the go file not the test file In the same package where that struct was defined because this is going to help limit circular dependencies that could arise When importing those fixtures in tests across packages Secondly the test naming convention a go test is written as a regular go function like this With the caveat that it always follows the same function signature So it's prefixed with the word test with capital T and it accepts a single testing dot t parameter Which is like the core testing package in go The first letter it should follow just like regular camel case. That's kind of goes Styling preference so the function name will identify that test routine like that test xxx and It should be capitalized So with all of the prerequisites we discussed in mind let's move on to writing tests and go We'll start out with very basic tests and work our way up to more complex use cases with like race conditions and all of that So paint patients as a virtue for the sake of Consistency I'm going to try and use the same example throughout so this Gophers getting pretty lit on beer beer seems to be pretty popular here in Belgium and I love beer So we'll go with that Our business model for our program can be an online beer store and subscription service For some context I created a few structs To represent each object in our business model So a cart is a shopping cart that contains a list of cases a case is a pack of beer that Can be a specific amount such as a six pack or a 30 rack and then a beer represents Details about the specific beer such as its brand its name its size and fluid ounces So on and so forth. So I've also written some preliminary fixture functions I've only pictured one here. There's some other ones in the code, but they're a little Intuitive once you see them in the tests And this is just going to allow us to rapidly initialize this struct So I don't need to take up six or seven lines of code creating this every time I want to create one of these objects Lastly the function we're going to test first is really simple It's called add case and it simply appends a single case of beer onto the list of cases in the shopping cart So our first test is pretty simple. We initialize a new empty cart again That function wasn't pictured, but it just initializes a cart object Before we do anything else. We're going to make sure the length is equal to zero with if statement and Call t dot fatal in the event that it fails So it'll print expected empty cart if for whatever reason there was beer populated in our cart to begin with. What's up? Yes typically yes only used for the test but if you if you want to use that fixture in Other packages like when you're say I'm testing in another package and I want to use that fixture I can't export that in the test in the test code itself It'll only look for the code in the go test or the go file. I'm sorry. Does that make sense? Okay, so custom error We'll create fixtures here So we can add those fixtures into our add case function The bat blue light is my favorite beer. It's a Canadian Pilsner Typically found in 12 ounce cans. So we'll fill that up in the case and If it works properly our test pass the second assertion as well because we're making that if statement on the length of the cart So as I just demonstrated in that last slide using the go testing package alone We can explicitly mark tests as past or failed depending on the if conditions This makes code more human readable But it can kind of create all of these like dangling conditionals that make it a little bit more tiresome And we're going to be writing a lot of tests today. So I want to introduce you to testifies assert and require packages Testify is a lightweight external dependency that acts as a testing toolkit around the go standard library testing package So it wraps the testing object T That's your top-level parameter in all of your tests and it extends common assertions such as equal not equal Error no error nil not nil and many other conditions Every function takes that object T as the first argument and as a result it can write those same errors out to the go tool the Assert and require functions also return booleans indicating if the assertion was successful or not So if you want to build off of each assertions and only make Assertions based on like what you just previously asserted you could do that. We're not going to do an example of that here We're just going to ignore and swallow the returned booleans, but for future reference if you have special code paths I would recommend that So the same test we just did it can be written in a few less lines of code simply by invoking testifies assert package So rather than making that if statement we can just assert equal the length of the cart and have This empty or expected empty cart message. So assert can take Three to four parameters the first being T the second parameter being the expect expected value the third parameter being the Actual value and then the fourth parameter being an optional like custom error message So that's what it'll print to the go test tool in the event of an error The require package is almost identical to the Assertion or the assert package the only difference being that if you call require dot Equal and that fails. It's similar to running T dot fail now which will short circuit the test in the event that it fails and it won't continue on So I'd recommend you kind of use your best discretion when deciding between an assert condition verse require condition But for any like non catastrophic failure scenarios It's probably easier to use assert so any failed test cases in some algorithm or some mathematical equation that you're computing Will still be surfaced despite any failures prior So we talked about test-driven development a little earlier now that we've gotten our feet a little bit wet with some go examples We're going to see it in practice So let's say I want to write a function called subtotal that calculates the subtotal of the contents in my shopping cart Instead of jumping directly to the dot go file. I'm going to start with the test dot go file writing a case that I know is going to fail Writing it before my code is going to encourage me to adhere to the original Expectations I have for the function and probably the original function signature as well so Test subtotal similar to the add case test I can fix your beers and cases and add them to my shopping cart Google tells me a duvel triple hop is a very popular Belgian beer and although it's quite expensive for a pack of four I'll be worldly and try it out and throw it in our cart so We also might be playing some drinking games later. We need a 30 rack of LeBat blue For 2499 so I'm expecting the subtotal function here Which we haven't yet written to accept zero parameters, so empty parentheses and Return a single value, which is a float If this function works as I would expect it to the calculated value should be 3998 but let's round up to 40 for the sake of this test so we can first assert the failure and then fix it later on So our test assumed the function signature was shown here. So this is how it should be defined So it should plug and play perfectly We're going to do some really intense math here to calculate the subtotal We'll iterate through each case in the cart adding the price to the subtotal and then return that value So it fails with an expected Value of 40 and passes with the corrected value of 3998 So while TDD kind of gets us to decent coverage in the small helper function I'd like to believe we're a little bit more thorough than that and we want to test more than one instance of the calculation so Although it may have passed like what happens if our cart is empty what happens if there's a negative value for some reason more than two cases duplicate cases in our cart and Then lastly, how can we capture all of these edge cases while simultaneously optimizing our code? reusability So for this purpose a common practice for unit tests and go are called test tables so you can create a test table which is basically a slice array of Input and output variables Defined directly within the test. So the struct doesn't have to exist anywhere else. It's just right here We can take the name of our test It'll accept a cart as an input value and the output value for the test case is going to be the subtotal Which is that float? So using normal go syntax and logic we can actually just iterate through all of the test cases here and T.run is going to Run each function as a subset or subtest of T in a separate go routine, but that go routine will block until each test is finished Doing this allows us to reuse this line of code But if you have longer tests if it's an integration test, it's going to be nice that you have Your test tables here because you're only going to have to define your testable stuff once So instead of it writing an entirely new test we can validate all of these multiple Edge cases as subtests so empty values multiple values negative values. They all abide by the same function signature, and we're good to go I'm not sure under our business model, but circumstances would create a negative value or like a free beer But in this case We're at least like aware of what we need to add and we probably just need like some additional validation in Either our fixture or in the struct itself So while we aim to compartment compartmentalize our code we can do the same for our tests So as a code reviewer, it's a lot more clear to identify like what your tests are actually trying to accomplish When they're broken out like this So the more complex the setup and verification the larger trade-off you get with some of this code reusability HTTP test is another standard library package which provides utilities for HTTP testing The new server function starts and returns a new server the caller of course being responsible for stopping and cleaning up that server It gets created with a unique URL So it's important that the component that you're testing has a way to pass that variable in so we can make our test against the test server Rather than a real server The function also accepts a HTTP handler that gets invoked each time the URL is called so we can Basically mock out the server and have it return different status codes So for example, let's think about how our business model might interact with like an external or arbitrary HTTP API So say we have a third-party service to process payments We can simply just do an HTTP post such as this to send the payload there It could include other things such as like the total amount That's due any credit card information if it's secure And whatever else the payments are we needs And this function as is is actually really difficult to test because pay.me is hard-coded directly Within the function itself, so we have no control over what the server is going to do So we wouldn't want to do this and Then it would just leave us really prone to like intermittent service errors and we wouldn't be able to actually like change the outcomes of the service so Passing the server address as an additional parameter is much more suitable for this test So the payment server here, it could still be a constant, but we're just going to pass it at the highest scope in the function So this will allow us when we're testing a process payment to pass the URL of the test server up here And we can control the custom handlers that are going to respond So as such we can simulate things such as auth errors server errors and of course like a successful payment processing So if we have more code down here that like is dependent on the response of HTTP post we can exercise those code paths as well The handler func type that you see up here is defined in the HTTP package And it basically is an adapter that's going to allow us to use ordinary functions that we write as HTTP handlers So if it abides by the appropriate signature here with a response writer and an HTTP request We'll be able to pass it in as the handler The signature it looks maybe a little weird if you've never seen this before But it's the same signature that if I was writing my own a go server I would run the function serve HTTP and it just mimics that as a real server So in each test case we'll write a custom function here and these two different handlers One for exercising a 200 status code So we'll do that with the right header function and we'll pass the status Okay, and then W dot right. Let's us pass a response body Through the test server and then here for our internal service error We'll just write that header and it won't have a response body Kind of similar to how a server might act in the wild And each subtest we'll start by initializing a new HTTP test server right here This handler funk is from that test case. So those little custom handlers we built Immediately we want to make sure that we defer T s dot close which means at the end of all of our tests or at the end of each iteration It's going to close the test server So it's ready to create a new one So for both of the test case that we defined in the last slide the expected errors And the expected body are asserted as well So it should match up But it actually doesn't and it's kind of a caveat to how the HTTP post method works Does anyone know why our test might fail? So HTTP post actually doesn't return If if it's like a 500 response code it still returns a nil error because it was actually successful in processing the post command so What we actually need is Some error checking or some checking around the response status code So if anything greater than a 400 we want our actual function to return that error So we can have this custom payment server error and give us what the status code is so The previous test would fail under I'm sorry it would pass under these conditions now and remember never trust the test You haven't seen fail So that caveat kind of reminds me of another gotcha that I've come across in the go world and it has to do with contexts Specifically how it relates to text testing So the context package in go is responsible for carrying like deadlines and cancellation signals across API boundaries and processes It's extremely powerful, but equally as important to understand So you want to initialize a parent context and you can call one of these two methods to do that They actually both initialize a non-nil empty context the main difference being Context dot background is typically done in the main function initialization and tests whereas to do kind of acts more as like a to-do like comment with the Idea that you will eventually like replace it in the future So maybe at the time that you're writing the function you don't have a context That's actually being passed down through a higher scoped function So you'll put a to-do there or your unsure of which context is appropriate to use But the intention is that we're going to replace that So it doesn't really make sense to use it to do in tests because we have no intention on replacing it And the test is already at the highest scope so If you ever notice inconsistent uses of to-do and background in tests that you're reviewing in the future Save someone from getting nerds sniped and direct them to background While they're programmatically the same if you use them correctly You can actually use like static analysis tools and it'll help validate that you're using and passing around context through your components Correctly, so it'll help surface problems early on So now that we know what context to use and when we can start creating functions which require them and test them out So context should always be the first parameter in a go function So in this start subscription timer, it's the first and only parameter Here I've implemented a subscription timer, which basically will listen for a context cancellation signal if it's done will just return from this function if not on every Interval of this ticker it will send a cart Along the message channel So you see it doesn't return here after so each each time that timer or that ticker expires It'll keep sending those messages on the channel So as a beer subscription service will have multiple orders to fulfill concurrently So we'll likely have to call start subscription timer through a go routine for each active subscription When the order is ready to be processed We just send it on that message Chan and we'll have another component of our code actually responsible for receiving those messages So how do we test channels? To test this function will create two unique carts there at the top For the sake of this test it makes sense to have a short interval duration such as one second or even like a higher granularity Like millisecond duration But that's how we set it through the subscription itself The timer is designed to run continuously and it doesn't return So like I said, we need to run this as a go routine in the test if we don't it's just gonna sit there and block and likely time out So our function is sending values along the channel But we need to verify that the correct message is actually being received on that message Chan so this channel syntax here is just gonna allow us to Just like pop that Message out of the channel and then we'll do a type assertion to make sure that it's actually a cart object and assert that It's equal to this cart up here So I've done this twice because I want to make sure that we can test consecutive Items and that the contents of this item is unique to the one that we defined earlier Now that we've verified our program can send values on a channel we want to work on a concurrent component That's designed to receive them. So we have this start order handler function It's again designed to continue continuously wait and handle any Messages that are received up here. So that same syntax that we use in our test For each cart that it receives on the message channel, it's gonna attempt to Place the order in this arbitrary function That's basically just going to iterate something in our order handler So if it fails, we'll log, but we'll continue on and stay in here If there's any type assertion failure, we'll log that as well The only time we're actually going to exit this program or this go-routine is if the channel itself is closed Which we can detect up there To test this asynchronous behavior We can simply start the order handler in a go-routine and send a few objects on the message chain So we start the function here Assert that the length is equal before we even start sending anything and then we send a couple messages on the message Chann you can see that this is a case object not a cart object, but we are guarding against Wrongful like type of assertions in our code So we know that there's only two carts that are processed and the length is equal to two. So this message isn't actually received Well, the message is received, but it doesn't count in our length So at this point we have two asynchronous and concurrent components of our project our subscription timer and our order handler They're both Basically small enough to fit in a single slide, which is kind of a good indicator that they're broken down and compartmentalized Which again, we said was really good So breaking things out is just going to make our lives easier in the future In a nutshell we have a working program with pseudocode that demonstrates some of those basics for writing go tests But we have yet to polish up on one of the harder concepts in go, which is race conditions and race detection Introducing that complexity kind of increases vulnerabilities such as race conditions whenever we're using concurrency such as go-routines and Channels I'm going to try and speed up a little bit again. We can use the dash race argument to detect race conditions Where Yeah, we'll just skip over that So we already have our tests written And I actually have a race condition in one of them already It was somewhat intentional. It's fine But there's a couple techniques that we can use to resolve unsafe memory access Such as blocking with weight groups blocking with channels returning a channel or using a mutex Some of these solutions are going to require me to refactor code and I don't really have time for that So we'll look at the stack trace and try to identify why it's failing Basically, this is just what it looks like it's saying on line 123 and line 138 in my go file on my go test that's what is Problematic and comparing these two lines. You can see that I'm attempting to read from the subscription cart in the main function and then in the test I'm trying to set the subscription cart and Mutate it and write to it. So that's really unsafe, especially when you have multiple concurrent processes that could access this So in this case, we're going to want to use a mutex To protect against us So the sync package, I wish I had a little bit more time to dive into the synchronization that it can offer, but it's basically intended for use by like low-level library routines Mutex stands for like mutual exclusion lock. So basically you can lock a value and unlock a value and locking it will Block any like reads and writes to that value until it's unlocked So we only need to make a couple more changes to make that test pass First we'll create we'll make sure the cart and interval variables are not exported So we can't access them outside of our package We'll add a mutual Mutual exclusion lock and then any time we want to we'll create those getters and setters and any time We want to either get the value of the cart or set the value of the cart We will lock it set it get it and then unlock it So that we'll just do that with the defer statement here. Sorry. I know we went over that a little bit fast But lo and behold synchronization protects our concurrent functions and the race detector okays our tests with just those simple changes such as a mutex so sync is just one of the many packages that We went over today and it's really really important. So I would definitely suggest going over that a little bit Sorry, I'm rambling on but I really appreciate you guys listening to me at nerd out on testing and Golang All of the examples you saw are up on my github, which is a nicky ex dove and Then a special thanks to all of these folks that contribute to open source with their gopher con or their gopher illustrations So thank you guys. I don't know if you have time for tests, but come find me out after