Testing API's and Loggers with Rich Steinmetz - RUBY 598
Rich Steinmetz is a creator at RichStone.io and is a Tech Coach. He joins the show to talk about his article, "Testing Rails loggers with minitest". He begins by talking about loggers and different ways to test them. Chuck also shares his ways of testing loggers.
Hosted by:
Charles Max Wood
Special Guests:
Rich Steinmetz
Show Notes
Rich Steinmetz is a creator at RichStone.io and is a Tech Coach. He joins the show to talk about his article, "Testing Rails loggers with minitest". He begins by talking about loggers and different ways to test them. Chuck also shares his ways of testing loggers.
Sponsors
- Chuck's Resume Template
- Raygun - Application Monitoring For Web & Mobile Apps
- Become a Top 1% Dev with a Top End Devs Membership
Links
Socials
Picks
- Charles - Smart Family Calendar
- Charles - Nikon D7500 DSLR
- Rich - Carcassonne | Board Game
Transcript
CHARLES MAX_WOOD:
Hey folks, welcome back to another episode of Ruby Rogues this week. I guess I am your host, Charles Max Wood, and we are talking to Rich, is it Steinmetz? Is that how you say your name? Or did I get anywhere close?
RICH_STEINMETZ:
Yeah, it was good in the American sense. Otherwise
CHARLES MAX_WOOD:
Right?
RICH_STEINMETZ:
the
CHARLES MAX_WOOD:
I'm not
RICH_STEINMETZ:
German...
CHARLES MAX_WOOD:
German, so...
RICH_STEINMETZ:
In German
CHARLES MAX_WOOD:
How
RICH_STEINMETZ:
it
CHARLES MAX_WOOD:
do
RICH_STEINMETZ:
would
CHARLES MAX_WOOD:
you say
RICH_STEINMETZ:
be
CHARLES MAX_WOOD:
in
RICH_STEINMETZ:
Steinmetz.
CHARLES MAX_WOOD:
Germany? Steinmet- oh, okay.
RICH_STEINMETZ:
Yeah, hello. Nice to be here for the first time.
CHARLES MAX_WOOD:
Yeah, absolutely. Now, I'm just gonna put it out there. We have talked plenty of times. You were in the book club, and we might talk a little bit about that. Anyway, I actually, I think I ran across your blog post back when I was working my previous contract and this blog post talks a bit about how to test loggers. And it was something that I hadn't really ever thought about. And just to give a little bit of context so that people kind of understand, you know. why
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
this became a thing at least for me. And then I'd love to hear your story about why you even cared about testing loggers in your own case, right? But,
RICH_STEINMETZ:
Mm-hmm.
CHARLES MAX_WOOD:
so what I was doing is I was writing integrations between two systems. So, you know, you have the API over here for the company that I was contracted to and an API over here for some third-party thing. And yeah, it was real exciting work, right? I pulled data out of one side and I translated it and put it in the other side, right?
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
And right,
RICH_STEINMETZ:
Well, I won't.
CHARLES MAX_WOOD:
yeah, fascinating work. Anyway, one thing that was kind of interesting about the way that it was set up though, was that they were using another piece of third party software to actually run the integrations. And so what they were hoping to do was effectively create kind of an app store for people integrating with their system to be able to integrate with any of these other third party systems, right? So, What they did was like shipping and logistics, and warehousing. And so on the other end, you might have some other ERP system or some other system that kept track of inventory or this or that, and some of these were like brick and mortar stores and some of them were e-commerce. And so having integrated cross so you could send data back and forth to and from Shopify or whatever. I'd list some of the other integrations I built, but you've never heard of these companies. I hadn't before I wrote them. So anyway, this system, the way that it worked was the only log you really had was standard out and standard error on the command line. But we wanted a logged format that would log the data. And then eventually we also hooked into... another logging system that I can't remember the name of, and it'll come to me at some point. So we wanted to check that it was logging to this internet system, and then it was logging to standard out and standard error using the format that we used and things like that. And so since that was the only information we had, and it kept a log, it kept that as the log, the output, logging became kind of important. So typically,
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
when I'm doing logging, I'm putting stuff out into a file, or Rails is doing it, or whatever. It doesn't matter, because ultimately, if the information's there and I need it to debug something then I'll do that. Even then I typically have a sentry or ray gun or something set up to capture actual errors. It's only if something happens that it doesn't generate an exception. and do I actually need
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
the logs? And so I usually just don't care about the logs. But in this case I did. And so I was out looking for a solution. Hey, how do I test the logger and make sure that the right information is getting put out there, right? And yeah, I found your stuff. And so...
RICH_STEINMETZ:
Did you find a good way to do that when you looked into it?
CHARLES MAX_WOOD:
There were a couple of ways that I found to do it, but before we get to that, I'm kind of curious what your story is. Like, what made you think, oh, I'm gonna test loggers, right?
RICH_STEINMETZ:
Yeah, most of the time, bloggers don't get much love, right? They kind of do their thing. And as you said, you just look into them whenever you need it and you don't really care. I noticed that sometimes, especially in this case, I did some test-driven development.
CHARLES MAX_WOOD:
Mm-hmm.
RICH_STEINMETZ:
And when test-driving something, and it was a very back-end heavy thing. In any case, whenever you test something, you want to test the behavior, right? And since this was like a very back-end heavy thing, the only behavior in some cases was that there was a log output.
CHARLES MAX_WOOD:
Okay.
RICH_STEINMETZ:
And the log output, well, sometimes it was really, really simple. Sometimes it was a little bit more complex with dynamic. expectations.
CHARLES MAX_WOOD:
Mm-hmm.
RICH_STEINMETZ:
And there were different cases that basically, as an example, there was a conversion in the specific blog post that you found. And this conversion could go in the right way. And then the user would have their duplicated, whatever it was, thing that they kind of copied. Or there could have been like seven things that go wrong, in which case the user, well, there's always one thing that happens, the user doesn't get their copy. But then there's also some part of the behavior that's important to us internally, which is, well, what did go wrong? It was really like crucial for us to know so we can recover. and we needed the extra information in the log and it was important for us to log the right thing. And this is basically where I started my journey with testing loggers. I did this before as well, actually, and I noticed that there are a lot of ways also to test loggers.
CHARLES MAX_WOOD:
Uh huh.
RICH_STEINMETZ:
In my blog post, I proposed one way which is... I'm not sure if this is something that from your story that you were looking for, because what I'm doing there, I'm just making sure that the right logger logs the right thing that basically the expected output is passed to the logger. And
CHARLES MAX_WOOD:
Mm-hmm.
RICH_STEINMETZ:
I kind of trust that logs to send it out or where, but then there is also this notion of you actually want to test that it. when to send her out. I think there's even like a mini test helper that does this.
CHARLES MAX_WOOD:
I don't know. I didn't test that either. I only tested that the information got passed to the logger function.
RICH_STEINMETZ:
Yeah,
CHARLES MAX_WOOD:
And.
RICH_STEINMETZ:
yeah, I think this is the easy route, right, to go and make sure that at least it's going the right way. There is, yeah, you could also, theoretically, you could also do if it's really important to you and maybe you, you know, there are people who test really everything, everything, everything. And you could even like check that file. has to ride out.
CHARLES MAX_WOOD:
Yeah, I mean that did occur to me, but it felt like overkill. All right, checking standard out
RICH_STEINMETZ:
It is,
CHARLES MAX_WOOD:
felt
RICH_STEINMETZ:
it
CHARLES MAX_WOOD:
like
RICH_STEINMETZ:
is.
CHARLES MAX_WOOD:
overkill. It looks like in your examples, I mean one of the things that I ran into was I was just using the Ruby logger class because
RICH_STEINMETZ:
Mm-hmm.
CHARLES MAX_WOOD:
I wasn't in Rails. Now it works basically like the Rails logger class. So, you know, the interface is pretty much identical. It's not. a stretch, but yeah, it looks like you were using Railslogger. And then the other thing I'll throw in is that... you were using mini tests and I was using RSpec.
RICH_STEINMETZ:
Yeah, that can also lead to different solutions. But in the end, I think it's a little bit more or less the same
CHARLES MAX_WOOD:
Mm-hmm.
RICH_STEINMETZ:
in terms of, yeah, you stop this thing out and you expect this and this input to go to Ruby log method or whatever method or instance you used. Yeah, I just saw there is... a assert log method in, in mini test and you give it a, a string as a parameter, as an argument. And then inside of the blog block, you can
CHARLES MAX_WOOD:
Uh-huh.
RICH_STEINMETZ:
run your, your code and it tests for basically the search
CHARLES MAX_WOOD:
Mm-hmm.
RICH_STEINMETZ:
things. the right thing got locked there. Yeah, so this is kind of built into mini-test and I used it in one of my testing workshops for a bootcamp that I did, but I don't know if RSpec has a similar thing, actually.
CHARLES MAX_WOOD:
Yeah, I don't know. I didn't know that MiniTest had it. So
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
I was testing it the way that you were as far as just doing, mine was a little more involved. I actually would create a mock. And so I would use the double method in RSpec to create a mock of the logger.
RICH_STEINMETZ:
Mm-hmm.
CHARLES MAX_WOOD:
And then I would just say it should receive info with whatever line. And
RICH_STEINMETZ:
Mm-hmm.
CHARLES MAX_WOOD:
so, Then if I had the formatting built into my logger, right? Cause I just, I created my own logger class and I totally cheated. I made it into a Singleton. I know people are throwing up in their mouths when I say that, but
RICH_STEINMETZ:
Thank you.
CHARLES MAX_WOOD:
what it meant was that I only had to load it one time, right? So I could go and I could tell everything, hey, include logger. And then when I did logger.new or whatever, or logger.instance, I think is what it is for Singletons. Right, it just, I didn't have to reinstantiate it because it already existed in the program. But yeah, so then I just, it always returned that double or that mock and then from there I would just test it and I would say, hey, did it receive this message? And what's interesting is that I could also mock the Ruby logger and so I would pass the message into my logger, my custom logger. And then I could test that the Ruby logger actually got the formatted message with the data that I write, because it reformatted it and put a timestamp on it and stuff. And so I could test
RICH_STEINMETZ:
Mm-hmm.
CHARLES MAX_WOOD:
them both. But yeah, I don't see anything in our spec that does the assert log. So I had to, I had to do test doubles.
RICH_STEINMETZ:
Yeah, yeah, yeah, it's interesting. Did you use like the Singleton in your app? So basically, all the logs went through the same instance, right?
CHARLES MAX_WOOD:
Mm-hmm. Yeah.
RICH_STEINMETZ:
Of the logger. Yeah. Yeah, well, what I did here, what I've done here is, I've used some dependency injection there.
CHARLES MAX_WOOD:
Mm-hmm.
RICH_STEINMETZ:
And it wasn't initially my idea. In Rails, you know, you can just do like Rails logger, and then does Rails logger actually every time create a new instance?
CHARLES MAX_WOOD:
I don't know. I actually don't
RICH_STEINMETZ:
I
CHARLES MAX_WOOD:
know
RICH_STEINMETZ:
think
CHARLES MAX_WOOD:
the answer
RICH_STEINMETZ:
so
CHARLES MAX_WOOD:
to
RICH_STEINMETZ:
because
CHARLES MAX_WOOD:
that.
RICH_STEINMETZ:
I ended up, I might be wrong now, but I ended up finding it useful to have this as dependency injection to the logger itself because
CHARLES MAX_WOOD:
Mm-hmm.
RICH_STEINMETZ:
I remember at least in our case, if I didn't do that, then basically there was, it was two different loggers. using
CHARLES MAX_WOOD:
Mm-hmm.
RICH_STEINMETZ:
like Rails logger and then you could what you could do of course you could do Rails logger any instance or something like that
CHARLES MAX_WOOD:
Mm-hmm.
RICH_STEINMETZ:
and which would be kind of a mock as well and then you it would get the method and yeah it would just work for for any. But then the dependency injection allowed for using the exact logger.
CHARLES MAX_WOOD:
Right, that makes sense. Yeah, and so for
RICH_STEINMETZ:
And this
CHARLES MAX_WOOD:
those
RICH_STEINMETZ:
was
CHARLES MAX_WOOD:
that
RICH_STEINMETZ:
kind of...
CHARLES MAX_WOOD:
aren't familiar with dependency injection, effectively, what we're talking about is, and you can see it in the blog post if you wanna go read it, folks, but yeah, you have a keyword argument that's logger and it defaults to rails.logger, but what happens is, yeah, then you can pass in the actual logger you wanna test against, whether that's a test double or an object that's stubbed out. and you can get the behavior you expect without having to wrangle anything extra or figure out how Rails works.
RICH_STEINMETZ:
Yeah, and you end up doing a lot of this when you do TDD. Because, and like, quote, unquote, good testing, because it's like, it's said to be not an optimal thing to run any instance anywhere. There is a saying that if you have any instance somewhere or like a mock, that you have a design problem, most probably. And in this case, though, it was useful. And first, in the beginning, I didn't go for the dependency injection, but in a review, we figured out that could be a good thing because in this particular case, we also, the logger was also supposed maybe in future to be configurable.
CHARLES MAX_WOOD:
Mm-hmm.
RICH_STEINMETZ:
And yeah, the, in this case, Whenever you have something that's configurable and can be called from several callers can be good to, to make it like a dependency injection in sense of instead of like a hard dependency inside of the instance.
CHARLES MAX_WOOD:
Yeah. Yeah, it makes a lot of sense. Most of the time, so I've seen dependency injection used, like you're saying, for testing. I think the other place that I've seen it is. Angular uses it pretty heavily. And
RICH_STEINMETZ:
Okay.
CHARLES MAX_WOOD:
so.
RICH_STEINMETZ:
No.
CHARLES MAX_WOOD:
But you know, and so you can drop different services in and swap out API systems and stuff like that. So,
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
but yeah, I mean, it makes sense here cause yeah, then you can just, you can control what logger you're using and which APIs you're expected to hit.
RICH_STEINMETZ:
Yeah, I'm curious, you said that you needed this kind of a thing, but you usually don't test logging because you just use it whenever it comes out. But you needed this in this particular case because there was a little bit kind of a team requirement for that or...
CHARLES MAX_WOOD:
Yeah, yeah, so the logging output was actually significant to being able to check the work. that the work
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
was done, right? It was, you know, the, the logging was actually saying we grabbed this object and translated it and put it here. And when then we
RICH_STEINMETZ:
Mm-hmm.
CHARLES MAX_WOOD:
grabbed this log object and grabbed it and translated and put it here. And then after a while we actually, it was all evented. And so then it was, Hey, we picked up the next event and triggered it and executed it and, you know, it did its thing. And so. If so, the, the system was I have to say, so the system they were using, and I'm not gonna say the name of the company, but it wasn't super reliable. And so that was the other piece of the issue was that you would have weird issues just running it.
RICH_STEINMETZ:
Mm-hmm.
CHARLES MAX_WOOD:
They had a hard 10 minute timeout, and so at 10 minutes, it literally would just kill your, I think it was running on Docker or something, I think it just killed the whole container. Yeah, I mean, midstream just. Right. And so if you wanted to know if you had 10 million records to get through on a run, right, it would batch them through. But you wanted to know how far it got and you know, that it picked up the next one on the next run. And so, you know, and then it properly logged because it was actually, you know, keeping track in another system. We finally built that. we built all these systems to compensate for the unreliability of this main system. But, you know, eventually we were logging to the other system that the event was completed, right? So that it would not trigger again. But yeah, for a long time, I mean, that was the only way to know, was to go and actually search the log. And so, you
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
know, they'd go look in the one system and, oh, it's not over here. So then they'd, you know. they'd look in the log and say, did it grab it, did it move it? And the answer was usually, yep. But you know, or hey, there was an issue putting that one in because the network went out in the middle of it, which happened more than once. So.
RICH_STEINMETZ:
Go out. Yeah, that's something.
CHARLES MAX_WOOD:
So yeah, so it was a critical component to being able to communicate about what was getting done for the team,
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
so.
RICH_STEINMETZ:
Did you, by the way, have you worked a lot with mini tests before? Yeah. I did.
CHARLES MAX_WOOD:
I have, I have, so this last contract, they were RSpec. My current contract is RSpec, but before that, when I was working for Morgan Stanley, all of our tests were written in Minitest.
RICH_STEINMETZ:
Okay,
CHARLES MAX_WOOD:
And some
RICH_STEINMETZ:
maybe
CHARLES MAX_WOOD:
of them,
RICH_STEINMETZ:
you can...
CHARLES MAX_WOOD:
some of them were written in the class, and then def test, whatever, whatever. And some of them were
RICH_STEINMETZ:
Mm-hmm.
CHARLES MAX_WOOD:
written with Minispec. you know, syntax. And so I've used them both.
RICH_STEINMETZ:
No.
CHARLES MAX_WOOD:
within the last year three.
RICH_STEINMETZ:
Maybe you can refresh my memory because I'm actually on my first, well, I'm doing it now for one and a half years. It's my current contract. I'm writing almost only mini tests and also my personal project. And before that, I did one and a half years RSpec. And there are a couple of examples in my blog post where... Well, the logger logs calls the info method three times, for example,
CHARLES MAX_WOOD:
Mm-hmm.
RICH_STEINMETZ:
right? Because it's always, I kind of send an info to the center out. And so in many tests, I really kind of struggled, especially in the beginning with the syntax where you. And maybe there's a better solution to that. And I kind of think it just. more readable and more workable in our spec, but maybe it's not. But when you do these mocks where you say logger expects like info
CHARLES MAX_WOOD:
Mm-hmm.
RICH_STEINMETZ:
three times, and then you need to say with, and then you need to pass a block in which you, um, well, how do you say it to the words where, um, you have the In the block, you have the parameter as the actual string that's passed to the info method. And then you need to kind of compare it with inputs that you expect. And for this, you need like an array where you put in the inputs and you need to shift it. It's probably very... complicated to explain it in words.
CHARLES MAX_WOOD:
Right. Well, I'm looking at the code, so I kind of follow what you're saying.
RICH_STEINMETZ:
Okay.
CHARLES MAX_WOOD:
And to be honest, I mean, a lot of this is just gonna come down to taste. But yeah, so essentially, yeah, you wind up looping over the array and telling it that, yeah, it has to have an expected value that matches the next thing in the array.
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
typically what I wind up doing, unless I just have a really long list of things, in which case I start looking at my test and evaluating whether or not my test is actually testing more than I need tested, right? Because you don't have to test every minute detail of everything, right? You just, it has to be able to give you confidence that your code is correct. And so,
RICH_STEINMETZ:
Oh yeah.
CHARLES MAX_WOOD:
but yeah, I've done this where I've put outputs, you know, strings or... values, objects, Rails models into an array, use the factory
RICH_STEINMETZ:
Mm.
CHARLES MAX_WOOD:
to create them. And then, yeah, typically the way I've done it though is I actually just loop over the array and then call essentially in your case, logger expects to be called with info three time or without the three times, right? Expects to be called with info with the first, with the item. as I loop over it. And often it just wants those to be in order.
RICH_STEINMETZ:
Mm.
CHARLES MAX_WOOD:
In our spec that seems to be what I remember. And so if I call that the
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
logger expects to be called with the value and then logger expects to be called with another value and then logger expects to be called with another value, as long as those values are in order, it's okay. And so... I often won't even use the loop if I'm calling it three or four times. It's when you get into, okay, I have a dozen of these, right? Then I start looking at the loop and going, okay, I'm willing to sacrifice the readability of having the string right there where I'm asserting something in order to keep the test concise. But.
RICH_STEINMETZ:
Yeah, yeah, the thing is, I also remember doing it, what you are saying, and it seems to me like the more reasonable thing to do. But at least in this, I'm using here Mocha library and MiniTest, right? So
CHARLES MAX_WOOD:
Mm-hmm.
RICH_STEINMETZ:
at least with these technologies, you end up if you have two expectations basically two mocks and the mock library calls it a mock right so if you have two mocks with the same method name it can determine kind of which one you are referring to or it takes the the first or the last or something like that and then you are bound to the syntax of saying logger expects method name so
CHARLES MAX_WOOD:
Mm-hmm.
RICH_STEINMETZ:
many times or an arbitrary amount of times or whatever. And yeah, you end up with this whole block thing and comparison thing with
CHARLES MAX_WOOD:
Yeah.
RICH_STEINMETZ:
the array, stuff like that. Yeah, it's interesting.
CHARLES MAX_WOOD:
Yeah, I opt for readability over concision in my code as much as I can. I mean, if
RICH_STEINMETZ:
Hmm.
CHARLES MAX_WOOD:
you, if you have a hundred things that you're testing, your test is probably way too big for one, or some of those tests should be moved into unit tests, right? And so then your integration test, you're not testing that it's logging a bunch of times, you're just testing the basically the back and forth between the programs or between the elements in your code. But yeah, so I don't mind having long test methods or long test blocks, right, in order to just be absolutely clear this is what I'm looking for, so. So, I'm gonna go ahead and do a little bit of a
RICH_STEINMETZ:
Yeah, yeah,
CHARLES MAX_WOOD:
but
RICH_STEINMETZ:
yeah,
CHARLES MAX_WOOD:
doing a loop
RICH_STEINMETZ:
in the
CHARLES MAX_WOOD:
or
RICH_STEINMETZ:
big.
CHARLES MAX_WOOD:
something like that, a lot of times that somebody who's been doing Ruby for 10 minutes can figure it out.
RICH_STEINMETZ:
Yeah. In the beginning, you also laughed a little bit about me saying that I loved the API stuff, but I think at least I actually meant it. Um, like APIs
CHARLES MAX_WOOD:
Oh.
RICH_STEINMETZ:
are one of my, one of my,
CHARLES MAX_WOOD:
APIs
RICH_STEINMETZ:
um,
CHARLES MAX_WOOD:
are
RICH_STEINMETZ:
yeah.
CHARLES MAX_WOOD:
interesting, but yeah, go ahead.
RICH_STEINMETZ:
Sending data around that's too simple or too rough work. Sometimes it's rough work. Yeah, right.
CHARLES MAX_WOOD:
Yeah. So the issues that I had were basically two problems. One was that the API documentation, I was going to say it was poor documentation, but it wasn't actually correct documentation. And so when it was wrong, it cost me days to figure out what the difference was.
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
And reaching out to their support, I would get a reply about a week later. And so by then, I'd usually figured it out.
RICH_STEINMETZ:
Oh wow.
CHARLES MAX_WOOD:
And on the occasion where I hadn't, right, I'd been banging my head against the wall for a week and that was frustrating. But the other issue is, is that, yeah, I mean, if you have a straightforward API that you're translating data from into another straightforward API, it's really not that interesting of work because it's literally go get this JSON and go translate it to this other JSON, right? And so you're not doing work that's really that interesting except for occasionally. the mapping gets a little bit hairy. And so that's kind of fun because then you're attacking this. Oh, how do I get this data that they put on this entity? You know, they store it in this nested entity in this way and it's weird. And so, yeah, you have a problem to solve, but yeah, I just, I didn't find it
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
that interesting. But
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
working with APIs itself, that's fun. Like right now I'm working on expanding. So there's an active campaign gem. um, active campaigns and email system that I use for top end devs.
RICH_STEINMETZ:
Mm.
CHARLES MAX_WOOD:
Right. And so I'm going to start,
RICH_STEINMETZ:
No.
CHARLES MAX_WOOD:
if you're on my list, you're going to start getting emails from me again. Yay. Um, but yeah, so what I was working on was in top end devs. If you go to the top end devs main site, there's a form there that you can put your email address in and it'll send you an email that says, if you use this RSS feed link, then you can get seven episodes of a, um, Otherwise unavailable podcasts that'll teach you how to advance your career and build momentum in your career, right? Because this is something that I've talked
RICH_STEINMETZ:
Nice.
CHARLES MAX_WOOD:
to dozens of people about and it's like, I feel stuck. I don't see how I'm gonna advance in my career. I wanna be senior developer. I'd like to get paid more, right? And so I was like, well, hey, you know, do these things, you'll build your skills, you'll build your network and you'll build your personal brand and people will hire you. And so, you know. Yeah, there are seven or eight episodes. I'm finishing the last of them tonight and then I get them all posted and stuff. But anyway, so if you put it in, I want the email to go out with the RSS link, but then I also want to subscribe you to a campaign, an active campaign that says, hey, here's episode one and here's what it's about. And then you get episode two and here's what it's about. And so I had to connect to the active campaign API and all of the active campaign gems that I could find were out of date except for one that one had really done was it had taken your API key for active campaign and translated that into a token and then basically gave you convenience methods on REST client, I think is what it's built on.
RICH_STEINMETZ:
Mm-hmm.
CHARLES MAX_WOOD:
But you still have to specify, so you don't tell it, I want to create a contact. You tell it, I want to post to the endpoint called slash contact with this JSON data, right? And then it just takes care of the. auth part of it for you. Well, I wanna be able to say, active campaign, double colon, contact.create, here's the data, right? And
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
so I've been building on top of that, right? And so that kind of API works kind of fun because it's like, hey, how do I make it do a thing, right? How do I go and, you know, how far can I take this as far as automating this process? So that's fun.
RICH_STEINMETZ:
Yeah. Yeah. Yeah. At my current company, Webinar Geek, we, it's a Webinar SaaS software,
CHARLES MAX_WOOD:
Mm-hmm.
RICH_STEINMETZ:
and we do quite a bit of integrations. Also, one of them
CHARLES MAX_WOOD:
Right.
RICH_STEINMETZ:
is also ActiveCampaign. And it's, yeah, it's kind of, I also find it fun to also find the right wrapper around
CHARLES MAX_WOOD:
Mm-hmm.
RICH_STEINMETZ:
the API. And then, yeah, like you said, make it do the right thing. that it looks the right way on the other end for the user. For your personal kind of project, by the way, I was today at Top End Devs on the home page and I watched a video that looked really good.
CHARLES MAX_WOOD:
Mm-hmm.
RICH_STEINMETZ:
I also left my email there. For the API that you are integrating now, like do you... Do you do some testing? What's your approach generally maybe in terms of testing APIs apart from loggers?
CHARLES MAX_WOOD:
So I'm probably gonna make some people sad because I don't love all of the options. And to be perfectly honest, right, if I'm running my tests, I don't want it hitting a live API somewhere. And as far as I've been able to see, like with ActiveCampaign, they don't really have a sandbox setup. Like when I'm writing stuff against Stripe, they, you know, I put in a test key and it does the stuff, right? And so... If I'm doing like a full on integration, hey, this plays nice with Stripe and I know that it plays nice with Stripe, it's because it will go do stuff on Stripe, right? But the problem is, is that those tests tend to take longer, right? Because it's talking over the internet and it's, you know, and so I have to wait for a response and then, you know, what have you. And it doesn't take too terribly long, but if I have a large number of tests, then it will take a while, right? So I've used VCR. Um, as a gem and there are things I like and things I don't. Um,
RICH_STEINMETZ:
Hmm.
CHARLES MAX_WOOD:
if I remember correctly, the last time I used it, you could actually automate it to, so that every like 10 days or every, you know, however long or however many tries it'll, it'll go get you a different, like it'll delete the cassette, the Jason file and make a new one. And so you're
RICH_STEINMETZ:
Yeah, maybe.
CHARLES MAX_WOOD:
never that far out of date.
RICH_STEINMETZ:
maybe for context, like VCR
CHARLES MAX_WOOD:
Right.
RICH_STEINMETZ:
does record the actual API requests into files and saves it basically in your repository. And then all subsequent tests will use this YAML file. And yeah, it's called a cassette. And then you can automatically re-record it. Sorry,
CHARLES MAX_WOOD:
Right.
RICH_STEINMETZ:
it's rough to do that.
CHARLES MAX_WOOD:
No, I appreciate it because yeah, I always forget that we have people that are new to Ruby even these days, right? And so yeah, it's good to remember. But yeah, so I've used VCR and there are a lot of things I like about it. But typically the way that I approach it, because that's one level of testing it, right? Because effectively you wind up testing it. at the level of using HTTP, HTT party or rest client or net HTTP. Right. And so that's, that's what VCR is testing is, you know, Hey, you know, at the lowest level, here's the data you get back. And then here's how you, you know, so you're, you're testing whatever integrations live on top of it from there. Right. So if you have like what I'm working on, where it translates active campaign responses into objects that I can. I have a bunch of convenience methods on that I can do stuff with. So I can test those and I can say, okay, get me the thing and it just, it rehydrates a cassette. But typically what I have is I have some layer on top of the API request like the direct, here's the path, here's the data, here's the auth. So go make the request for me. I have something sitting on top of that, right? And so in this case, it's, I forked the active campaign simple client, I can't remember
RICH_STEINMETZ:
Mm-hmm.
CHARLES MAX_WOOD:
exactly what it's called, but it's something like that, that gem, right? So I have a fork on it on GitHub, and I've just barely started adding this stuff. And so I probably either try and PR and contribute it back, and then if that's not the direction that the author intended it to go, then I'll just fork it and make. his client dependency of my client.
RICH_STEINMETZ:
Okay. Yeah.
CHARLES MAX_WOOD:
Or just keep
RICH_STEINMETZ:
It's just.
CHARLES MAX_WOOD:
things as they are and then just deviate where I want. But anyway, so yeah, so I've got like a contact class on top and I've got a tag class on top. And so, you know, I have a method on contact that's add tag, right? And so then it goes and it makes
RICH_STEINMETZ:
Mm-hmm.
CHARLES MAX_WOOD:
the call to contact tag. I don't. I don't know why, I shouldn't complain about their API, but I don't understand why ActiveCampaign doesn't have a create contact with a tag option on it so you can just do it in one call, but they don't, right? So I have to create that, and if the tag doesn't exist, I have to create it, and then I have to put them together.
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
Or I can go add them through the UI, and then, you know. But anyway, so, yeah, so then what I wind up doing is I just wind up testing. that level of thing. And then, yeah, I use some form of, I wouldn't say it's dependency injection, so much as it's, so typically I have like a retrieve method and a new method, and so the new method will take the data structure, the JSON, from the request. And so what that means is that if I want to unit test, the, you know, the contact class and make sure that it's giving me the data back or what have you, or makes the proper call under the hood, then I can mock out the call library because I assume that it works, right? I assume the HTT party or REST client does the right thing. I'm not gonna test that, they test that, right? I test that when it doesn't work as I expect, is basically when I
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
hit it. So yeah, so I just stubbed that sucker out. so that it doesn't make a call out to the endpoint on the other end, and then I'll test everything in there. And then, yeah, usually I'll have some level of integration tests that says, hey, if you retrieve this, it should behave this way. But then I'm not testing
RICH_STEINMETZ:
Hmm.
CHARLES MAX_WOOD:
every method on there. I'm just testing, hey, I got data back as I expected, and I'm getting this contact with this information and this stuff. But even then, It's tricky, right? Because a lot of times you're doing authentication that requires secrets, right? So you have an API key or you have, you know, like a public key and a secret key like Stripe does or something like that, right? And you don't want those in your tests. And so what you have to do is you have to give the user a way to configure it with a.env file or something so that they can run the tests on their own system. but then you also have to give them a method. And this is why I don't love some of this stuff is because then you have to give them a way of effectively seeding the data into the system in the first place in an easy and repeatable way in order to do it. Or you wind up having those VCR cassettes and
RICH_STEINMETZ:
Hmm.
CHARLES MAX_WOOD:
just making sure that they're checked in so that when they run it, right, it always uses the stored data. But the other issue you run into with VCR is that a lot of times, like for me with the, you know, I'm doing developer training. And so I don't fall under any regulations other than like the generic ones like GDPR or California's privacy law. I think Virginia passed a privacy law here in the U S right. And so I have to comply
RICH_STEINMETZ:
Mm-hmm.
CHARLES MAX_WOOD:
with those if I want people in those areas to buy my stuff, but I don't have to comply with like. FERPA, which is student privacy in the US. Or, you know, I use Stripe for my payment system, so I don't have to fall under PCI compliance, right? Because I never, that data never hits my system, and so I don't have to protect it. But it's possible for me to run a VCR against a system that does pull back some kind of like student data or something like that, right? And so I have to be
RICH_STEINMETZ:
Mm-hmm.
CHARLES MAX_WOOD:
careful from there, right? Because I don't want to give away anybody else's information either, right? Now, if I always hit and I always get a contact that's me and it's my information, right? I can give that away if I want. But yeah, you know, if I have it hit active campaign, is the example we've been using, or something else, and it pulls your information and puts it in a VCR cassette, then that's not okay. And so you have to play... This is the issue that I run into, right? And so, sometimes what I do is I just pull the VCR cassette in once and then I modify the data before I commit it. And then I just tell it never to refresh it. But the problem you run into
RICH_STEINMETZ:
Mm-hmm.
CHARLES MAX_WOOD:
with that is then if the API changes or if the data output changes, then you're testing for a scenario that's no longer valid. And so it's a tricky thing to handle. The other thing that I have done though, is I've stubbed out doing the HTT party request or the rest client request, and then effectively just testing the return values to make sure that they are what I want, right? So. If I'm only pulling the contact email address, name, and phone number, then I only test those fields. And that way, if the overall response, they add other fields to it or things like that. I just, I'm just not testing those. Right. So as long as those three things remain consistent, then I'm okay.
RICH_STEINMETZ:
Yeah,
CHARLES MAX_WOOD:
And then, you
RICH_STEINMETZ:
yeah,
CHARLES MAX_WOOD:
know, maybe have
RICH_STEINMETZ:
basically...
CHARLES MAX_WOOD:
some live tests off to the side that I run. just to make sure they run without errors.
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
Right.
RICH_STEINMETZ:
Yeah, yeah, yeah. Okay.
CHARLES MAX_WOOD:
So
RICH_STEINMETZ:
So
CHARLES MAX_WOOD:
those
RICH_STEINMETZ:
basically...
CHARLES MAX_WOOD:
don't run without my keys in there.
RICH_STEINMETZ:
Yeah. Okay. You have like two free approaches, basically. One that
CHARLES MAX_WOOD:
Yeah.
RICH_STEINMETZ:
you would go with maybe for as a heavyweight solution, doing the VCR thing, because it needs some setup, it needs additional work. If you're on a team, it also needs like additional education. It's kind of a little bit of a mind shift.
CHARLES MAX_WOOD:
Yeah, typically I stub it first, but yeah. I'll
RICH_STEINMETZ:
Yeah,
CHARLES MAX_WOOD:
go
RICH_STEINMETZ:
okay.
CHARLES MAX_WOOD:
to VCR, the other approach, the full approach if it warrants it, and sometimes it does.
RICH_STEINMETZ:
Yeah. Okay. And then, yeah, stubbing would mean you have your client, so to say, that talks with the API, for example, the
CHARLES MAX_WOOD:
Mm-hmm.
RICH_STEINMETZ:
ActiveCampaign gem gives you a client that has all these methods, create this, get this, and then you make sure it's not like you stub it, it's not making the request, but you give it the parameters,
CHARLES MAX_WOOD:
but it gives back a compatible
RICH_STEINMETZ:
whatever.
CHARLES MAX_WOOD:
data structure on the other end, yeah.
RICH_STEINMETZ:
Yeah, yeah, it can be like an object or whatever active campaign returns like a hash or an object or then you have to kind
CHARLES MAX_WOOD:
Right.
RICH_STEINMETZ:
of Implement it yourself.
CHARLES MAX_WOOD:
Right. And so typically what happens there is like the HTTP, HTT party, keep wanting to say HTTP party. I'm just not going to worry about
RICH_STEINMETZ:
No.
CHARLES MAX_WOOD:
saying it. Right. And you all know what I mean. Um,
RICH_STEINMETZ:
No.
CHARLES MAX_WOOD:
but yeah, so the HTTP party
RICH_STEINMETZ:
I wouldn't even know
CHARLES MAX_WOOD:
response
RICH_STEINMETZ:
what's wrong.
CHARLES MAX_WOOD:
is its own object. Right. And so it responds to its own things. And so I have to stub out the response and say, you're going to, return an object that responds to the same things as the response object from HTT Party, right?
RICH_STEINMETZ:
Uh huh.
CHARLES MAX_WOOD:
And so, you know, whatever calls I'm doing internal to my library, they all get responded to by the stub because Ruby does duct typing, which is awesome. And so,
RICH_STEINMETZ:
Hmm.
CHARLES MAX_WOOD:
it's kind of the reverse of dependency injection, right? It's like, I can give you any object as long as it responds to whatever I'm calling against it. And so I just make sure
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
it's compatible with my stuff. And then I hope it doesn't go stale. And
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
mostly that hasn't bit me. Typically what gets me to reach for something like VCR is if they are consistently iterating on their... responses or if the response is exceptionally complex and I have to deal with the complexity, then I'll grab a VCR and say, just store the whole thing.
RICH_STEINMETZ:
Hmm.
CHARLES MAX_WOOD:
And then, yeah, I do like to run against a live system periodically, but sometimes... Sometimes the systems are so complicated, it's just not worth trying to do it, right? The return on your time just doesn't make sense. And so if it's a fairly simple API, and it's mission critical that I have the sanity check, then yeah, then I'll say, okay, every week you're gonna run these tests too, and they hit the live system, and we just make sure that we're still getting back what we expect. But typically then
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
I have some dummy account, or like with ActiveCampaign, I'd have a handful of contacts that are tagged with a specific tag that's just a testing tag. Right? And so I know
RICH_STEINMETZ:
Mm-hmm.
CHARLES MAX_WOOD:
not to change those, right?
RICH_STEINMETZ:
No.
CHARLES MAX_WOOD:
Because the test is going to goof with them. And then anything I change from the test, I change back at the end.
RICH_STEINMETZ:
Yeah, interesting. I always create new accounts basically.
CHARLES MAX_WOOD:
Yeah, sometimes I don't
RICH_STEINMETZ:
and
CHARLES MAX_WOOD:
wanna pay for an extra account. If I can
RICH_STEINMETZ:
Oh
CHARLES MAX_WOOD:
do
RICH_STEINMETZ:
yeah,
CHARLES MAX_WOOD:
that, that's
RICH_STEINMETZ:
if it's
CHARLES MAX_WOOD:
what
RICH_STEINMETZ:
bait.
CHARLES MAX_WOOD:
I do, right? But
RICH_STEINMETZ:
Okay,
CHARLES MAX_WOOD:
sometimes,
RICH_STEINMETZ:
if it's bait, I understand.
CHARLES MAX_WOOD:
yeah, sometimes it's like, hey, yeah, you can have another account, it's 30 more bucks. It's like, for testing, no, no.
RICH_STEINMETZ:
Wow. Yeah, this is the issue with smaller APIs, whereas bigger APIs always have some development account like, I don't know, Stripe, Salesforce, right? They always
CHARLES MAX_WOOD:
Yeah.
RICH_STEINMETZ:
give you
CHARLES MAX_WOOD:
Yeah, they'll
RICH_STEINMETZ:
like
CHARLES MAX_WOOD:
give you
RICH_STEINMETZ:
a free
CHARLES MAX_WOOD:
a sandbox.
RICH_STEINMETZ:
version.
CHARLES MAX_WOOD:
I think PayPal,
RICH_STEINMETZ:
Exactly.
CHARLES MAX_WOOD:
you can set up a sandbox. So
RICH_STEINMETZ:
Yeah,
CHARLES MAX_WOOD:
yeah.
RICH_STEINMETZ:
but in those cases, the value
CHARLES MAX_WOOD:
There's shoes.
RICH_STEINMETZ:
of using VCR is again, like what you said, right, it's considerably lower because their APIs are inherently very, very stable.
CHARLES MAX_WOOD:
They're very
RICH_STEINMETZ:
So
CHARLES MAX_WOOD:
stable, yep. And
RICH_STEINMETZ:
value goes
CHARLES MAX_WOOD:
the other
RICH_STEINMETZ:
down.
CHARLES MAX_WOOD:
thing is that they're also widely used enough to where the gems handle pretty much everything. So I'm just testing everything that I put on top of that gem, right? And I just stub
RICH_STEINMETZ:
Hmm
CHARLES MAX_WOOD:
out the stuff I get from the gem. So I don't even have to worry about the API.
RICH_STEINMETZ:
Yeah. Yeah. Sometimes the, the good or the nicer thing of still using the VCR or an advantage is that you need to write less code,
CHARLES MAX_WOOD:
Mm-hmm.
RICH_STEINMETZ:
like less stop code because it records once it's all there. And if your method does 10 calls, and you try to test drive it or to change it, you always have these stops that you need to maintain and ride on
CHARLES MAX_WOOD:
Right.
RICH_STEINMETZ:
the way. So this is like the disadvantage of using
CHARLES MAX_WOOD:
Mm-hmm.
RICH_STEINMETZ:
stops.
CHARLES MAX_WOOD:
Yep, absolutely. And it's, that's why a lot of it just depends, right?
RICH_STEINMETZ:
Hmm.
CHARLES MAX_WOOD:
So yeah, I mean, people look for the silver bullet. There really isn't one. And sometimes you just kind of get stuck in this place with the API where it just sucks because it just, there's just not a nice way to do what you wanna do.
RICH_STEINMETZ:
Yeah. Yeah.
CHARLES MAX_WOOD:
And I've given feedback to places like, your system is no fun to work on, right? And it's like, hey, if you change these handful of things, keep the old API so that anybody using them can still use them, but create these nice APIs, right? Put it out there as your 2.0 or put it out there as, hey, we had some, you can tell them I'm an expert and just say, hey, we had an expert tell us that these things would make it real real convenient to work on. So we're adding these endpoints, but the old endpoints work and anything that depends on those endpoints will continue to work. We're not doing anything with them, but yeah.
RICH_STEINMETZ:
Yeah, changing APIs is like its own story. Have you ever run into cultural, like team cultural things where you would like to do testing in a certain way, test your loggers in a certain way, or test your APIs in a certain way, but then it's kind of, you see that there is. It will be difficult to adopt in a team. Have you run
CHARLES MAX_WOOD:
Right.
RICH_STEINMETZ:
ever, maybe with something like a VCR or something?
CHARLES MAX_WOOD:
So... Typically, when I'm on a team. they either have something that's already in place, right? And so if that's the case, I can just come in and say, hey, I'd like to do this. And everybody just kind of goes, all right, right? Most people are willing to at least try it. Or, and this is the much, much, much, much more common case, they're not testing.
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
And if they ever did, those
RICH_STEINMETZ:
Oh.
CHARLES MAX_WOOD:
tests are old. And so
RICH_STEINMETZ:
Oh
CHARLES MAX_WOOD:
in that case, I can do whatever I want. But the problem is, is that since I'm the only person running the tests, if somebody else breaks the test, then I may complain and not get any traction on getting it fixed.
RICH_STEINMETZ:
Oh yeah.
CHARLES MAX_WOOD:
So,
RICH_STEINMETZ:
That's tough.
CHARLES MAX_WOOD:
and getting buy-in from that level, it's hard because effectively they look at it and they see where things are at and all they see is a ton of work that doesn't actually move things forward. And no matter how
RICH_STEINMETZ:
Mm.
CHARLES MAX_WOOD:
many times you explain to them, hey look, having these tests run is a sanity check that we won't break stuff and it allows us to move faster. because at the end of the day, it keeps us from adding technical debt in a number of meaningful ways. And so we ought to do it. It just, they either believe it or they don't. And by believe it, not say they believe it, right? They actually feel it, right? They feel the momentum that they get from it, right? And one example of this, and it was with this last client, right? They had... And I'm not gonna go into all the issues in the code base, right? And I mean, it worked and got the job done, but they had a bunch of globals throughout the program. They had, I think the method was 150 to 200 lines that did most of the work. And it still farmed stuff out to other classes. Just stuff like that. And so, I put tests around a lot of it and then came in and cleaned up. a lot of it because it made it easier to test, but it also made it a lot easier to read and work on. And so all of a sudden our velocity went up because of the positive changes in the code. And so sometimes it's not just, hey, we're confident in our changes and therefore we can move faster, but it was also we could find where stuff got done because it made sense. where to look for it, right? And the code was easy enough
RICH_STEINMETZ:
Hmm.
CHARLES MAX_WOOD:
to follow it to where, if you didn't know, you could find it real easy, right? It's like, oh, well, it's obviously gonna be part of this process. And then you look at that, okay, it's gonna be part of the second step of the process. Okay, this is making a direct call to a class that says that it does exactly what I'm looking for it to do. Right, you know, and
RICH_STEINMETZ:
Mm-hmm.
CHARLES MAX_WOOD:
you were on the book club, were you on the book club when we talked to Bob about clean architecture? You know, a lot of this stuff
RICH_STEINMETZ:
Mmm,
CHARLES MAX_WOOD:
comes out of clean
RICH_STEINMETZ:
no,
CHARLES MAX_WOOD:
code.
RICH_STEINMETZ:
no.
CHARLES MAX_WOOD:
Okay,
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
but a lot of this stuff comes out of clean code, right? So it's named well, it's neatly partitioned into components and all the stuff that we talked about there. But yeah, so it makes a difference in that way as well. And people kind of have to see it to understand it. So getting people to buy into testing, yeah. I mean, I just went and did it when I was waiting for them to answer a question I had, or I went and cleaned it. cleaned it up when I was hung up on something and waiting for the support on one of the APIs to get back to me or things like that.
RICH_STEINMETZ:
Hmm.
CHARLES MAX_WOOD:
And it was a pretty massive, I didn't get it completely done. And in fact, when I left, when the contract ended, I submitted like six PRs, right? Because I just kept finding and cleaning more stuff. And so I was like, hey, this is gonna get you like 80% of the cleanup that you needed, right? But yeah, you know, them seeing it, it was like, oh, you know, you saw some light bulbs go on. But yeah, getting buy-in is, if you're already doing testing, typically you can say, hey, I'd like to try this in our testing, and people are like, oh, okay. But
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
if you're not, then they don't care what you do, unless somebody else is running the tests as well, but it's also hard to get them to see the value.
RICH_STEINMETZ:
Yeah, yeah, it's funny that you say it was the testing buy-in because I'm in my bubble of having worked in teams, at least in the Rails world where they were into, and Ruby world where they were into testing and it's also sad that the Ruby communities a lot into testing and then yeah, every now and then I hear about Rails projects or Ruby projects where there is no tests and I'm just like, I can't even imagine anymore how it is, how exciting it is to push code to a large code base that doesn't have tests. Actually I would like to experience this again someday, but on the other hand, I'm really happy to. work on a well-tested code base that actually gives me some confidence in what I'm pushing out there.
CHARLES MAX_WOOD:
Yeah, well, and that's the other piece is that the value of testing goes up with more people working on the project, right? Because then at that point I can encode my assumptions. And then if you come in and you misunderstand one of the assumptions that I made when I wrote the code, that's critical to its functioning and you break the assumption, it'll tell you, right? Instead of coming back later and going, why did this break? Who was the last person to work on this? How do I even track down where the problem is, right? It just comes up and says, hey, you failed two unit tests here and an integration test. So you're, you know, this is what you broke, right?
RICH_STEINMETZ:
Yeah. And this is why I also like, now we are getting a little bit into the nitty gritty details, but I really like to, in my test names, to really say what I expect, the assumption,
CHARLES MAX_WOOD:
Mm-hmm.
RICH_STEINMETZ:
like you said it, like
CHARLES MAX_WOOD:
Yep.
RICH_STEINMETZ:
the actual assumption of what should be returned and not like testing this
CHARLES MAX_WOOD:
Yep.
RICH_STEINMETZ:
method.
CHARLES MAX_WOOD:
Yeah. Test. When I do this, I get this. Right.
RICH_STEINMETZ:
Yeah, thank you.
CHARLES MAX_WOOD:
So anyway, yeah. Well, we're kind of toward the end of our hour. We've been talking for a whole hour. Can you believe that, Rich?
RICH_STEINMETZ:
No, no I can't.
CHARLES MAX_WOOD:
But yeah, I mean, I could talk about testing and stuff all day, and I have to admit that some of the projects that I've started since I was the only person working on them, I didn't start them with tests. And I wish I had now, right? Because they've either gotten complicated or I'm looking to bring somebody on to work on some of the project to add stuff to it. And it's like, I don't even know how to begin to tell them what all is in here. And then the other thing is that Um, there's a certain level of, of complexity that I want to take out because it was like, okay, I started going down this road and then I figured out there was a better way, but I'd already committed part of the other solution and I'm not using it. Right. And if I'm not using
RICH_STEINMETZ:
Hmm.
CHARLES MAX_WOOD:
it, I don't want anything to run it. And the best way to know that it's not getting run is to take it out. So, um, and I'm sure testing to a certain degree can help you with that too. You know, just from the standpoint of, you know, I mean, Rails makes it somewhat easy because usually things are named after what they are. And so it's like, okay, like in top end devs, there's an admin user class. And I combine the admin app with the user or with the regular app. And so nothing accesses the admin user anymore. So I just need to put in a migration to drop the table and rip it out because nothing touches it anymore. So I've got dead code in
RICH_STEINMETZ:
Mm-hmm.
CHARLES MAX_WOOD:
there and we could talk about that too, but I don't know. Just, it's kind of hard
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
to track that stuff down.
RICH_STEINMETZ:
I will see this as an invitation to fulfill my dreams of pushing code into a code base that doesn't have a lot of tests,
CHARLES MAX_WOOD:
the
RICH_STEINMETZ:
if you have some lying around. But
CHARLES MAX_WOOD:
Uh,
RICH_STEINMETZ:
it
CHARLES MAX_WOOD:
yeah.
RICH_STEINMETZ:
also goes back to what we said in the beginning, well, you can test your bloggers, you can test your APIs, you can test things that don't exist anymore. But ideally it would be good to always keep your tests on target and
CHARLES MAX_WOOD:
Mm-hmm.
RICH_STEINMETZ:
to make them test behavior and test actual things that users will experience.
CHARLES MAX_WOOD:
Yeah, a hundred percent. All right, well, let's go ahead and move on to pics. So I'll go ahead and start us off. I've got all kinds of stuff to pick. So last week was Mother's Day, or this Sunday was Mother's Day, this last Sunday. And I got my wife a couple of things. She's really into gnomes. And so
RICH_STEINMETZ:
Nopes.
CHARLES MAX_WOOD:
gnomes, they're the little
RICH_STEINMETZ:
Like
CHARLES MAX_WOOD:
men
RICH_STEINMETZ:
the
CHARLES MAX_WOOD:
with the
RICH_STEINMETZ:
little
CHARLES MAX_WOOD:
beards and the
RICH_STEINMETZ:
thing?
CHARLES MAX_WOOD:
hats.
RICH_STEINMETZ:
Okay.
CHARLES MAX_WOOD:
And so yeah, I got her some gnome stuff, but the other thing I got her was, and this is kind of cool, and I'll put a link to it in the show notes, but it's called Skylight, and you can get it on Amazon, you can also get it directly from them. I think I hit an ad on Instagram that gave me a discount, but I got the Skylight calendar. And I'll put a link in here. So what it does is it... And I can go into some of the other stuff too. I got some kind of funny related stuff to talk about. Anyway, it's a calendar for the family. And then it also does chore tracking. You can put your shopping list in it. You can put a to-do list in it. So my wife and I are looking at remodeling part of the house. So we may put some of the to-dos in there for that. What else does it do? Meal planning. Did I say that already? Anyway, so
RICH_STEINMETZ:
No.
CHARLES MAX_WOOD:
it, and it just comes with a little stand, so you just sit it on your counter. The one I got is 15 inches. Don't ask me how many centimeters that is because I haven't, I don't have a clue.
RICH_STEINMETZ:
30 something
CHARLES MAX_WOOD:
So,
RICH_STEINMETZ:
probably.
CHARLES MAX_WOOD:
probably. So, anyway, so yeah, so, you know, it's just, it's kind of like an Amazon Echo, it sits on your counter. and does all that stuff. You can also add pictures to it. And then, you know, you can set it up so that when it goes to sleep, it'll rotate through your pictures or videos or whatever you put on it. And, you know, I have five kids and I've been fighting them to get the chores done. That was another thing that was nice is that I had set up a spreadsheet where I was keeping track of when they done their chores. It's more blank than I wish it was. this allows them to just go into it and just check them off. And so when they think it's done, they can check it off. And then I can go look at the chore and I can let them know what they didn't do. And then I can go and uncheck it, right? So that's nice just as a way to kind of have some accountability on that stuff. And one thing that I figured out, cause initially I was just adding the chores to each day and then I figured out that you can tell it to. do it on a weekly basis because our chores rotate every week. And so, and some of the chores you only do like on Tuesday, Thursday, and Saturday, like if you're washing a toilet or vacuuming a floor or something like that. And so I could tell it, hey, rotate this every, you know, every five weeks, right? Because the chores just rotate one kid to the next. And so, yeah, I have to set it up five times, right? But then I'm done. And yeah, it has all the right chores on the right days. So... Anyway, I'm pretty happy with that, pretty excited about it. It was like 300 bucks. So if you're looking for a solution to that, I'm just so tired of fighting the chores and stuff. So yeah, I'm gonna pick that. I
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
usually pick a board game and I'm trying to think what I've played lately. things have been a little bit insane the last
RICH_STEINMETZ:
I can
CHARLES MAX_WOOD:
few weeks and I haven't played
RICH_STEINMETZ:
take
CHARLES MAX_WOOD:
with
RICH_STEINMETZ:
a...
CHARLES MAX_WOOD:
my friends.
RICH_STEINMETZ:
I can pick one for you too.
CHARLES MAX_WOOD:
Oh, go ahead.
RICH_STEINMETZ:
Well,
CHARLES MAX_WOOD:
You have a board game?
RICH_STEINMETZ:
you probably, maybe you picked one, this one already some time
CHARLES MAX_WOOD:
Could
RICH_STEINMETZ:
before,
CHARLES MAX_WOOD:
be.
RICH_STEINMETZ:
but I also have two kids. Well, the smaller one is not ready yet for board games. It's not even two. The bigger one is
CHARLES MAX_WOOD:
Mm-hmm.
RICH_STEINMETZ:
six. And it went quite well with him to play Carcassonne.
CHARLES MAX_WOOD:
Oh.
RICH_STEINMETZ:
It's a game where you build little streets and you build these roads and then you close them off and the more of your own kind of colors are on the streets that you've built that are closed
CHARLES MAX_WOOD:
Uh-huh.
RICH_STEINMETZ:
the more points you get and yeah the better the the sooner you get all the points that you can sooner you win and it worked out well with my kids. So that's a good one
CHARLES MAX_WOOD:
Very
RICH_STEINMETZ:
if you
CHARLES MAX_WOOD:
cool.
RICH_STEINMETZ:
have kids.
CHARLES MAX_WOOD:
Now that game has been around for a really, really long time and I have never played it.
RICH_STEINMETZ:
Yeah, it's
CHARLES MAX_WOOD:
So,
RICH_STEINMETZ:
a good one. Yeah, exactly, it's this one.
CHARLES MAX_WOOD:
I've
RICH_STEINMETZ:
And
CHARLES MAX_WOOD:
heard
RICH_STEINMETZ:
we
CHARLES MAX_WOOD:
good
RICH_STEINMETZ:
played
CHARLES MAX_WOOD:
things.
RICH_STEINMETZ:
the junior edition.
CHARLES MAX_WOOD:
Yeah, I think I'm gonna type it into board game geek, but it looks like, oh, it's only been around since 2000. Anyway, it
RICH_STEINMETZ:
Wow.
CHARLES MAX_WOOD:
looks like it says that seven or older can play it, so that
RICH_STEINMETZ:
Yeah,
CHARLES MAX_WOOD:
means all of my kids
RICH_STEINMETZ:
it's
CHARLES MAX_WOOD:
can play
RICH_STEINMETZ:
a...
CHARLES MAX_WOOD:
it.
RICH_STEINMETZ:
Yeah, we played the junior edition, I think it
CHARLES MAX_WOOD:
Uh
RICH_STEINMETZ:
starts
CHARLES MAX_WOOD:
huh.
RICH_STEINMETZ:
a little bit earlier there. I don't know what difference is, but...
CHARLES MAX_WOOD:
I don't
RICH_STEINMETZ:
It
CHARLES MAX_WOOD:
know,
RICH_STEINMETZ:
worked.
CHARLES MAX_WOOD:
usually what happens is they take a rule or two out that make it hard.
RICH_STEINMETZ:
Hmm
CHARLES MAX_WOOD:
So then, you know, the kids don't have to manage as much complexity in their heads to come up with a strategy
RICH_STEINMETZ:
Oh yeah.
CHARLES MAX_WOOD:
that can win. It also means that it's pretty easy for adults to nail down and win. But yeah, it looks like it's Board Game Geek waits it at 1.9, which is, you know, kind of a family-friendly casual game. 30 to 45 minute gameplay, two to five players. So yeah. Very cool. Yeah, we played a game that has artwork that's similar to this and I'm trying to remember what it's called. I might've picked it. So if I think of it, then I'll pick it out, but otherwise. Yeah, and then the other pic that I have, so I have two more pics. One is that I just bought a new camera. It's right here actually. And it looks, so if you're watching the video, it's a Nikon D7500. The camera that I have used for the videos to date that's up there is a Nikon D5600. And so the 7500's a few years newer, The feature that I needed is, and the reason that I'm not using the camera right now to record Ruby Rogues and use the green screen and everything, besides the fact that I need to change out my lights, is that the D5600 will turn itself off after a half hour, and you cannot make it go longer than that. And so if I wanna record a longer video and use the green screen and everything else, then I actually have to tell it to wind the shutter to turn back on, right?
RICH_STEINMETZ:
Okay.
CHARLES MAX_WOOD:
And so my screen will go black in the middle of the recording.
RICH_STEINMETZ:
Wow, why is that?
CHARLES MAX_WOOD:
I think they're worried about it overheating and stuff. The D7500, you can set it to just not turn off. Now, they warn you, right, that it might overheat if you do that, but... since the only thing I'm using my camera for is effectively for it to be a really nice webcam, right? Cause it connects to the Blackmagic ATEM mini, right? And then streams into my computer. So, you know, I can get away with having it on for an hour or so. And so that's one thing that I'm changing up. I also bought some lights on Amazon. So I got the camera on eBay and they said it was just the base unit. Like that was it, said base unit only. It wasn't the base unit only. You saw it had a lens on it. I don't know if the
RICH_STEINMETZ:
Mm.
CHARLES MAX_WOOD:
lenses are compatible, but if they aren't, then I'll just use this to do my other photography stuff. Let my kids go take pictures with it. It was kind of an expensive camera. But anyway, so yeah, so I got it on eBay, so I'm just gonna shout that out too. And then the other pick I have, so I did order some lights. And if you go to top end devs right now, by the time you watch it, I don't know if I will have changed it out or not, but one of the issues that I had was that you can see the ring lights behind me if you're watching the video. And if not, I have ring lights behind me. And then I've got some lights up when I stand up that shine on my head. Well, the problem is, is that the light's kind of harsh. And so what happens is, is if I'm wearing my glasses, And I like the way I look with my glasses on. I'd rather record. I feel like I look haggard without them on. It casts a shadow from the glasses onto my face. And so
RICH_STEINMETZ:
Mm-hmm.
CHARLES MAX_WOOD:
my issue was that I needed kind of a softer light. And so what I wound up buying is I bought these lights there. I'll put a link in the show notes. They had a kit with three lights in it. So one is set up to kind of, or two of them are set up to be kind of your main key light. Right, so if my right side of my face was gonna be lit up, you're getting way more information than you wanted. I am rambling. And I'm sorry, but I'm gonna keep rambling until I'm done.
RICH_STEINMETZ:
Thank
CHARLES MAX_WOOD:
But
RICH_STEINMETZ:
you.
CHARLES MAX_WOOD:
anyway, so the nice thing about the kit is that... Yeah, so it has two of those key lights and then it has a third light that's on a boom that you can use to light from the side, okay? And so, and then the other thing you probably ought to know is that in order to properly use a green screen, you have to light the green screen too. And you have to light it in a way that does not cast a shadow from you onto the green screen, otherwise you get a different color green and it's harder to make it work, right? And so I was running into issues there because I had to broaden the color of green that it would use. And so, and then the shadows on my face. Anyway, it was actually putting some of the background onto my face, which was weird, but I posted the video anyway. And I'm super picky. So anyway, so these lights will allow me to light everything evenly because it's a softer light and so it spreads. And because it's a softer light from a larger area, it doesn't cast a sharp shadow on my glasses, right? On my face. Cause it'll light it from above it and below it. And so, anyway, that's why I got new lighting and I'm looking forward to recording some stuff. I'm planning on recording a crap ton of the... The developer career momentum podcast, which is the one that you get when you enter your email address on the website And then. the other thing I'm gonna be doing is the catapult your coding career. And I've recorded a few of those. I actually recorded some of them in my truck. I guess I should pick those microphones too because the microphone I use in my truck just plugs into my phone, which is nice. So I'll pick that too. And so I'll tell you about that in a minute. But yeah, so that's what I'm using it for and that way I can put up a video with some kind of interesting background. And I can also. You know, I can also record whatever, however I want, and I can do it on the podcast and stuff like that. So the microphone, it's actually a little lavalier mic. I'll have to search my orders here real quick. But what it is is it has, and I'll hold it up to the camera, but I can just explain it if you're on audio only. But it has a receiver, and it's just this little, I mean, it's just a little piece of hardware, right? And it has an adapter for my iPhone, right? It has the USB-C
RICH_STEINMETZ:
Like a dongle.
CHARLES MAX_WOOD:
and it has an adapter. Dongle, yeah. And so I just plug it into the bottom of my phone, and then it came with two Lavalier mics. that I just clip onto my shirt somewhere and then I can just talk at it and it sounds good. I mean, it doesn't sound like my microphone that I'm talking on now does, but if I'm out and about, then I can use it. And the thing that I like there is I'm also gonna be putting more videos up on Twitter and TikTok and Facebook and Instagram and stuff like that. And so those will be like 60 second videos that probably aren't gonna wind up. on the podcast feed, but they'll wind up as shorts and reels and whatever else you call them on YouTube and what have you. So, you know, it'll just be a, hey, I saw this and it made me think of that, right? And so, you know, you're gonna get a lot of object lessons there. I did one just talking briefly about something. I did a test run of them and I thought they sounded great. And they were like 36 bucks, $36. Amazon. So that was a good deal. Anyway, I am picking a ton of equipment that you probably all don't necessarily need to care about. But in case you're trying to get into recording and if you get into top end devs and you start doing what I recommend that you do, you will. You know, these are all things that can kind of help you out. I am going to be traveling to Amsterdam in about a week, get a half, two weeks. I think it's two weeks.
RICH_STEINMETZ:
Nice to have
CHARLES MAX_WOOD:
So,
RICH_STEINMETZ:
you here
CHARLES MAX_WOOD:
yeah,
RICH_STEINMETZ:
in Europe.
CHARLES MAX_WOOD:
right? So, you know, yeah, I can just record stuff in the airport or whatever, right? I can just, you know, pop the microphone on me, you know, find a place where the lighting doesn't totally suck, and, you know, record a quick video and just be like, hey, I'm on my way, or, you know, hey, there was a lady that walked by that was wearing a weird t-shirt that didn't fit her right. and it made me think of testing or whatever, whatever I come up with. So anyway. But yeah, so those are my picks. Sorry, I rambled a ton. Y'all asked for it, I guess.
RICH_STEINMETZ:
Oh
CHARLES MAX_WOOD:
I
RICH_STEINMETZ:
no.
CHARLES MAX_WOOD:
haven't been on this show for a few weeks just because I've been fighting all this other stuff. So I'm back. Rich, what
RICH_STEINMETZ:
Yeah,
CHARLES MAX_WOOD:
are your picks?
RICH_STEINMETZ:
it's a whole, it's a whole science, right? With the video audio set up. We are also having our curious
CHARLES MAX_WOOD:
Yeah.
RICH_STEINMETZ:
coders Chronicles podcast where we try to figure stuff out and while we are doing it now for a couple of months, we are still trying to figure stuff out. So this
CHARLES MAX_WOOD:
Right.
RICH_STEINMETZ:
is actually a very helpful workshop and like recommendations, maybe as part of your. course there or like
CHARLES MAX_WOOD:
Mm-hmm.
RICH_STEINMETZ:
RSS feed
CHARLES MAX_WOOD:
That's
RICH_STEINMETZ:
there.
CHARLES MAX_WOOD:
the plan.
RICH_STEINMETZ:
Maybe you could have like a session about setup and things like
CHARLES MAX_WOOD:
Yeah,
RICH_STEINMETZ:
that. It would be
CHARLES MAX_WOOD:
I'm
RICH_STEINMETZ:
interesting
CHARLES MAX_WOOD:
planning
RICH_STEINMETZ:
for sure.
CHARLES MAX_WOOD:
on having, so on Top End Devs, I'm planning on having one of the series be about building your personal brand. And so I'll talk about this equipment and stuff, but if you get into it and you start doing things like I tell you to, you're gonna start ugly, right? You're not gonna go buy gobs of expensive equipment, right? You're gonna go get a 20 or $30 microphone that is better than your onboard computer microphone, and you're gonna start recording. right? And you may wind up using the webcam that's built into your laptop and that is totally fine, right? What you'll figure out is as you go, it's like, oh, okay, I've done like 10 or 15 of these, right? And that's where I got with the video stuff because I've been doing audio for 13 years. I've been doing, or no, not 13. I've been doing audio for 17 years, but you know, the video stuff was all new to me and the green screen stuff was all new to me, but I wanted to have some fun with it. I wanted to try some new stuff. And so I started ugly, right? I started with the lights I already had, right? Because my wife got me the ring
RICH_STEINMETZ:
Hmm.
CHARLES MAX_WOOD:
lights for Christmas a couple of years ago, and I don't even remember what I bought these other lights for, but they just didn't work out, right? So I kept recording stuff with them until the other lights came, right? So the other lights are waiting to be set up. And so, you know, I'll spend an hour or two setting up the lights instead of recording because I have them now. But in the meantime, I mean... Don't stress if you don't have the nice microphone and the nice, you know, whatever, right? It's, you know,
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
the microphone I started with was a $50 microphone. The microphone I use today is a $350 microphone. And the reason is, is because I use it all the time and I got to the point where I was using it as part of my business. And so it made a lot of sense to invest in something that sounded really, really nice. But if you're doing it to build your personal brand, and you just want to put content out there and it's kind of a hobby, you don't need a $350 microphone, right? You need something that sounds okay, right? And most of the microphones that cost more than 50 or 60 bucks, they sound fine. And nobody's going to think, oh, he's on a cheaper mic, right? The difference between this mic and the other mic, honestly, you're probably not, most people are not going to hear it. And the people who do hear it... are gonna have like some really high fidelity headphones on and they're gonna go, oh yeah, this sounds crisper than the other one. If they're really
RICH_STEINMETZ:
Hmm.
CHARLES MAX_WOOD:
into it and they really care and that show starts right after this one. So
RICH_STEINMETZ:
Oh yeah.
CHARLES MAX_WOOD:
yeah, I can't just let it slide with the, hey, I just picked all this equipment and make it sound like you need it because you don't, right? I'm doing it because I'm gonna be doing trainings with it. I'm gonna be doing podcasts with it. I'm gonna be doing YouTube videos with it and I can afford it. So. Anyway,
RICH_STEINMETZ:
Yeah, totally.
CHARLES MAX_WOOD:
sorry. Rent
RICH_STEINMETZ:
Also,
CHARLES MAX_WOOD:
over.
RICH_STEINMETZ:
I can just sign that getting started goes over everything
CHARLES MAX_WOOD:
Yeah.
RICH_STEINMETZ:
else. And
CHARLES MAX_WOOD:
Yeah.
RICH_STEINMETZ:
iterating and then improving over time is the way
CHARLES MAX_WOOD:
Yeah,
RICH_STEINMETZ:
to go.
CHARLES MAX_WOOD:
but I will put content
RICH_STEINMETZ:
In which
CHARLES MAX_WOOD:
up
RICH_STEINMETZ:
direction?
CHARLES MAX_WOOD:
and explain how to do it. Yeah.
RICH_STEINMETZ:
Yeah, yeah, that'd be cool. Um, and yeah, my picks also go to into a similar direction. I tried to make them quick. Uh, one of them is, um, the top in def's book club, uh, just as a little thank you for, for setting it up. I had a lot of fun with the sessions that we had together, um, and where we read the programmatic programmer book and we had a look like we had great conversations I think and then the authors even were there and I think before that Robert C. Martin was there so it's a pretty cool thing and yeah I hope there's you're doing right now the seven languages in seven weeks I
CHARLES MAX_WOOD:
Yep,
RICH_STEINMETZ:
think
CHARLES MAX_WOOD:
seven languages
RICH_STEINMETZ:
yeah.
CHARLES MAX_WOOD:
in seven weeks. And we had Bruce Tate, he actually came on the call on Tuesday and he's
RICH_STEINMETZ:
Oh.
CHARLES MAX_WOOD:
gonna come on again at the end of the book when we do Haskell, I think it's the last one. So he's gonna come
RICH_STEINMETZ:
Mmm.
CHARLES MAX_WOOD:
on for the Haskell chapter, but yeah, some people are like, well, can I come in in the middle of the book? Yeah, you can because you just read the chapter we're reading. This book's especially pretty. approachable that way because each book's each chapter is kind of self-contained as Far as learning the language thing so Anyway, definitely come check it out If you use the code Ruby Rogues, then you'll you can come do it for two weeks with us for free and then after that You know you have to pay but yeah,
RICH_STEINMETZ:
Nice,
CHARLES MAX_WOOD:
that's been super fun.
RICH_STEINMETZ:
nice.
CHARLES MAX_WOOD:
That's been one of the best things I ever did for myself is just be able to talk to all you cool guys about this stuff. I love having conversations about this stuff.
RICH_STEINMETZ:
Yeah, that's really cool. Yeah, and my other pick would be coding challenges. I kind of, I liked them when I just started getting programming, I don't know, seven, eight years ago. And then I kind of found them to be, or I thought maybe they're a waste of time. And I almost never, or avoided like interviews. and interview processes that are heavy on coding challenges. But then I thought I'll make it like a mental challenge for myself to beat the hardest coding challenge out there,
CHARLES MAX_WOOD:
Mm-hmm.
RICH_STEINMETZ:
or one of the hardest one, which is the TopTile coding challenge.
CHARLES MAX_WOOD:
Oh, interesting.
RICH_STEINMETZ:
And
CHARLES MAX_WOOD:
I haven't heard of that one.
RICH_STEINMETZ:
yeah, TopTile is a freelancer platform. And I tried this coding challenge three years ago. I failed miserably and since then it's stuck in my mind. And I just want to do it just not because I necessarily very much need to get on the platform, but just to beat the coding challenge and to beat my own brain. And because it's also specialized skill, it's a little bit of math, a little bit of puzzling, a little bit of this and that. And it can be fun actually. And. Yeah, I'll be doing that. I'll maybe be streaming that. Probably I should actually do, pick one language from the seven languages book and do it in this language challenge
CHARLES MAX_WOOD:
You do it
RICH_STEINMETZ:
there
CHARLES MAX_WOOD:
in Prolog.
RICH_STEINMETZ:
and visit you guys there.
CHARLES MAX_WOOD:
Yeah, absolutely.
RICH_STEINMETZ:
In the book probably. Yeah, this is what I'm up to at the moment. I don't know if I'll get this project done by the end of the year, because it takes time to get into this whole mindset of holding challenges. But yeah, let's see how it goes.
CHARLES MAX_WOOD:
Yeah, makes sense. Well, a few other things I just wanna throw out there if people are looking for you. It's richstone.io. That's where your blog lives.
RICH_STEINMETZ:
Oh, thanks.
CHARLES MAX_WOOD:
And
RICH_STEINMETZ:
Yeah.
CHARLES MAX_WOOD:
what's your Twitter and GitHub?
RICH_STEINMETZ:
It's richstone.io is my Twitter. And richstone, it came before richstone.io. So richstone is my GitHub.
CHARLES MAX_WOOD:
Very cool. All right. Well, thanks for coming on and talking and testing. I love diving into that.
RICH_STEINMETZ:
Yeah, that was great.
CHARLES MAX_WOOD:
Yeah,
RICH_STEINMETZ:
Thanks
CHARLES MAX_WOOD:
till next
RICH_STEINMETZ:
for the
CHARLES MAX_WOOD:
time,
RICH_STEINMETZ:
invite.
CHARLES MAX_WOOD:
folks. Max out.
RICH_STEINMETZ:
Bye.
Testing API's and Loggers with Rich Steinmetz - RUBY 598
0:00
Playback Speed: