RSpec Best Practices: Toni's Top Do's and Don'ts for Writing Great Tests

Summarized using AI

RSpec Best Practices: Toni's Top Do's and Don'ts for Writing Great Tests

Toni Rib • November 28, 2023 • online • Talk

In this talk titled "RSpec Best Practices: Toni's Top Do's and Don'ts for Writing Great Tests," Toni Rib shares insights gained from years of experience with RSpec and Test Driven Development (TDD). Rib addresses common issues in testing practices that often lead to test failures or confusion among developers. The talk emphasizes the importance of clarity and precision in writing both tests and test suites through the following key points:

  • Readability: Good tests should be easy to read and follow, ideally consisting of one assertion per test.
  • Accuracy and Correct Level: Tests should accurately reflect the behavior of the code being tested without unnecessary complications.
  • Determinism: Tests must be deterministic, meaning they consistently pass or fail without dependency on the order of test execution.
  • Three A's Approach: Following the Arrange, Act, Assert pattern helps keep tests organized and easy to understand.
  • Best Practices and Common Pitfalls:
    • Avoid leaving critical code paths untested and ensure tests document code behavior.
    • Be specific in testing expected behavior instead of using generalized assertions.
    • Limit the use of shared examples to avoid excessive complexity in test files.
    • Steer clear of asserting exact database counts and instead focus on the changes introduced by actions.
    • Avoid reliance on strict ordering in tests, particularly with database responses where the order of returns is not guaranteed.
    • Use robust data generation methods rather than relying on random data from libraries like Faker when test outcomes depend on specific values.
  • Environmental Setup: Reset environment variables in test contexts to avoid state leakage between tests.
  • Documentation in Tests: Titles in test blocks should be descriptive to aid understanding for both current and future developers.

Rib also shares insights on the complexity of query planning in databases and how that impacts test consistency. Concluding, he emphasizes that tests are a critical aspect of documentation within code, serving to clarify the intended functionality of software components. By adhering to these best practices, developers can create more reliable and maintainable tests that ultimately enhance software quality and reduce flakiness in test outcomes. Rib suggests exploring additional resources for deeper understanding, including his previous talks on non-deterministic specs.

RSpec Best Practices: Toni's Top Do's and Don'ts for Writing Great Tests
Toni Rib • November 28, 2023 • online • Talk

We've all been there: you're running tests in CI and a test not related to your PR fails for seemingly no reason. Or maybe you change code and the test that should have caught it doesn't fail. From many years of experience of using Test Driven Development to write tests, fixing hundreds of non-deterministic tests, and running the Testing Guild back when I worked at Gusto, this talk will cover my top things to watch out for while testing with RSpec.

https://www.wnb-rb.dev/meetups/2023/11/28

WNB.rb Meetup November 2023

00:00:00.240 this is a talk that I created back when
00:00:02.080 I was at my previous job at gusto in
00:00:03.919 like 2019 that I've given two or three
00:00:06.359 times since then in some format or
00:00:07.840 another um and if you're wondering why
00:00:09.960 is an infrastructure engineer talking to
00:00:11.480 us about arpec stuff um my background is
00:00:14.000 actually all in rails so I started out
00:00:16.720 doing um backend rails engineering kind
00:00:20.320 of moves to full stack rails and react
00:00:23.240 um and worked at a few different places
00:00:25.000 and then sort of switch into the
00:00:26.320 infrastructure devop space back in
00:00:27.960 November of 2020 um but still have kind
00:00:30.599 of worked in sort of a role that Bridges
00:00:32.480 the gap between uh like pure
00:00:34.360 infrastructure work and the engineers
00:00:36.120 working on the application so I still
00:00:37.680 kind of hop in from time to time to do
00:00:39.239 different things um and before I left
00:00:41.480 Gusto I was leading uh what we called
00:00:43.600 the testing Guild we're trying to
00:00:45.520 improve sort of testing practices around
00:00:47.239 and that was uh where I last gave this
00:00:50.000 talk um so to talk about testing
00:00:54.239 generally uh real quick there's
00:00:55.879 difference right between a test and a
00:00:57.840 test weite um and I'll pause here for a
00:01:00.600 second too and say this talk is very
00:01:02.440 participatory um we're going to get to a
00:01:04.280 lot of opinions that I have that I've
00:01:05.560 built up over the years around testing
00:01:07.320 so like I probably won't see a question
00:01:09.159 if you drop it in the chat but if you
00:01:10.680 want to raise your hand um I'll try to
00:01:12.720 be looking over and let people hop in at
00:01:14.479 different points
00:01:15.840 here uh so what makes a good test versus
00:01:19.040 a test Suite test should be readable and
00:01:22.079 easy to follow right you can tell what's
00:01:23.759 being tested easily and you don't have
00:01:25.840 to hop around to a bunch of different
00:01:27.320 places to find all the different parts
00:01:28.840 of your test
00:01:30.360 ideally they're small and you have one
00:01:32.320 assertion in there um I will say that
00:01:34.200 may not hold for end to end test or
00:01:35.840 feature tests where you're clicking
00:01:36.960 around but especially for unit tests you
00:01:38.840 want them to be small and concise they
00:01:41.200 should be accurate which means they
00:01:43.000 actually portray the behavior of the
00:01:44.520 code
00:01:45.399 correctly and it should be tested on the
00:01:47.799 correct level right so you're testing
00:01:49.719 what is actually under test and nothing
00:01:51.479 more again with feature test sort of
00:01:53.560 aside from that test Suites on the other
00:01:56.600 hand right now is the group your test
00:01:57.960 should be thorough so they should cover
00:01:59.840 all pass in your code in there any if
00:02:01.759 statement you have any tary operator you
00:02:04.000 have uh should all be covered in some
00:02:06.640 way in your test uh they should be
00:02:08.920 uncluttered and by that I mean no
00:02:11.039 unnecessary test setup right if you only
00:02:13.360 need two users and a test to test
00:02:15.599 something you shouldn't create five of
00:02:17.360 them um that makes it harder to
00:02:20.480 follow ideally your test rats are also
00:02:23.239 consistent um I'm sure people have
00:02:25.519 hopped into larger rails projects uh
00:02:28.280 where like in one
00:02:30.519 uh part of the code tests are written in
00:02:32.400 one way and another part they're written
00:02:33.599 in a totally different way and you kind
00:02:34.920 of have to like mentally keep all that
00:02:36.480 together so um I really try to on a team
00:02:39.400 sort of find a consistent pattern for
00:02:41.239 writing codes so that you just don't
00:02:43.480 have that mental overhead anytime you
00:02:45.040 hop into a
00:02:46.400 file uh and deterministic this is my
00:02:49.480 favorite one on here uh people like to
00:02:51.640 call test flaky which kind of implies
00:02:53.519 that like oh there's nothing we can do
00:02:55.000 about it they're just going to flake
00:02:56.080 some sometimes really they're
00:02:58.159 non-deterministic right so your test
00:03:00.200 should always pass or should always fail
00:03:02.680 and you shouldn't have any weird order
00:03:03.959 dependencies a their
00:03:06.480 thing I like to follow the Three A's uh
00:03:09.760 for testing if you haven't heard them
00:03:11.319 before so uh they are AR range which is
00:03:14.360 set up all the objects that need to be
00:03:15.959 tested along with any other necessary
00:03:18.040 setup an act so actually do the thing
00:03:21.319 you're trying to test and then assert
00:03:24.200 right so make claims about the object
00:03:26.159 that you're testing or any of its
00:03:27.720 collaborators on there um I tend to find
00:03:30.640 that if you follow this consistently
00:03:33.360 it's a lot easier to actually figure out
00:03:35.080 what's going on in your test um it can
00:03:37.040 be difficult when people start arranging
00:03:39.120 stuff and then acting and then maybe
00:03:40.280 arrange more thing and act more thing
00:03:41.599 they sort of doing a lot in test to be
00:03:43.080 able to actually follow what's going on
00:03:45.239 um and I'm sure since we're talking arpe
00:03:47.000 here uh the uh expect to receive always
00:03:51.799 gets very confusing especially for new
00:03:53.599 developers because you call it before
00:03:55.560 you actually act on a thing uh so you're
00:03:57.920 sort of flipping the order of a certain
00:03:59.959 act which I don't
00:04:01.280 love so some disclaimers about the rest
00:04:03.959 of this talk um all rules have
00:04:06.640 exceptions which is why I call these
00:04:08.200 best practices and not
00:04:10.040 rules uh not all the things I'm going to
00:04:12.599 talk about that I think you should do in
00:04:13.720 testing are widely practices at all the
00:04:15.319 places that I've worked and some of them
00:04:18.320 are generally accepted I would say some
00:04:20.000 of them are really like based on my
00:04:21.880 experience here uh so again feel free to
00:04:25.600 hop in if you want to disagree with me I
00:04:27.360 love being disagreed with on parts of
00:04:29.000 this talk
00:04:30.280 um the first one here is don't leave
00:04:32.759 important code paths untested right if
00:04:35.320 your method is two different if
00:04:36.919 statements having one test that only
00:04:38.680 goes through one path is not going to
00:04:39.919 help you catch any bugs there right it's
00:04:41.960 also going to make change for your code
00:04:44.800 uh harder later because you have some
00:04:47.199 existing functionality that's not
00:04:48.720 actually covered under your test so you
00:04:50.199 don't know if you're going to introduce
00:04:51.440 a
00:04:53.280 regression you should always test s code
00:04:55.440 past right your tests are documentation
00:04:57.440 in the end which is sort of one of the
00:04:58.720 things I like to Hammer home here um if
00:05:02.120 you are testing all your code pads and
00:05:04.120 there you are leaving human readable
00:05:05.919 documentation for what that code does
00:05:08.280 and what it should do in every case that
00:05:10.000 you have
00:05:11.759 there uh don't be so General that you
00:05:14.800 aren't testing the real Behavior your
00:05:16.479 code so I see this a lot where people
00:05:19.000 will say like expect this thing to
00:05:22.120 change some model. count by negative one
00:05:26.319 right uh which will pass if any instance
00:05:28.840 of that model is deleted which is
00:05:30.360 probably not the thing that you're
00:05:31.280 trying to test which is to say hey I ran
00:05:33.240 this method and it deleted this user
00:05:35.560 specifically right so instead you want
00:05:38.440 to be specific enough to actually cover
00:05:40.039 the use case that you're testing on here
00:05:41.960 so doing something like delete it then
00:05:43.880 expect that if you reload that instance
00:05:45.680 from active record you get the active
00:05:47.120 record record not found error right um
00:05:50.520 so it's important if you say like this
00:05:53.199 test is meant to delete this user that
00:05:54.720 you are actually doing
00:05:57.600 that uh this one tends to be a little
00:06:00.280 controversial um I hate shared examples
00:06:02.919 and shared context um I have seen in all
00:06:07.039 the real shops I've worked at that they
00:06:08.560 tend to be uh like start out small and
00:06:11.759 sort of helpful and then quickly become
00:06:14.080 very bloated I have seen shared contacts
00:06:16.319 referencing shared contacts referencing
00:06:17.800 shared contracts across like multiple
00:06:19.680 files there to the point where you have
00:06:21.720 no idea where these objects are coming
00:06:23.280 from uh that are being set up and to be
00:06:25.720 honest I just don't want to go to a
00:06:27.240 different file uh when I'm looking at
00:06:29.039 tests like I want to be able to just
00:06:31.840 have everything there in one
00:06:33.919 test um I'm sure everybody's heard the
00:06:36.479 dry acronym right don't repeat yourself
00:06:39.240 uh when it comes to test files
00:06:41.479 specifically I like to do the opposite
00:06:44.360 which is wet which is widely explicit
00:06:46.440 test right sometimes when you extract
00:06:49.759 these things out like a shared example
00:06:51.960 um to have you know less lines of code
00:06:53.840 you can drop in for all these different
00:06:55.560 use cases you're actually hiding the
00:06:57.639 fact that you might be able to like
00:06:59.879 extract some functionality there that
00:07:01.639 would allow it so you don't have to
00:07:03.000 repeat yourself so by making it really
00:07:04.919 easy and dry you're potentially like
00:07:07.759 hiding uh an extraction that could be
00:07:10.599 that you might have noticed otherwise
00:07:12.759 right um so I personally shy away from
00:07:15.919 these uh at my first team at Gusto I
00:07:17.919 just banned people from using them and
00:07:19.840 refus to approve any PR on our team that
00:07:22.560 had them that's how strongly I feel
00:07:24.560 about
00:07:26.080 that uh don't ever assert absolutely
00:07:29.560 database counts um if a method adds a
00:07:33.000 record to the database you don't
00:07:34.879 actually care right like how many things
00:07:37.560 were there before that right the thing
00:07:39.560 you're actually testing is that
00:07:41.160 something got inserted to the database
00:07:42.919 has nothing to do with where there's
00:07:44.120 zero things in there before five things
00:07:45.840 in there before right so I see a lot of
00:07:48.599 more Junior developers do something like
00:07:50.840 assert user. count is zero and then do
00:07:54.039 something and then assert user. count is
00:07:55.960 one right which can make the test very
00:07:58.720 brittle also because if somebody puts a
00:08:01.159 let bank statement at the top of the
00:08:03.000 file for a different test let's say that
00:08:05.360 includes a user now this existing test
00:08:08.360 is going to fail when it shouldn't have
00:08:10.400 really right has nothing to do with that
00:08:12.280 so um the change match is the best way
00:08:15.400 to get around this right you can expect
00:08:17.159 something to change by one and then you
00:08:19.879 don't have to care about how many things
00:08:21.159 were there
00:08:26.039 beforehand uh it's also can be pretty
00:08:28.319 brittle to really explicit date and
00:08:31.279 times uh tests can flake I seem to
00:08:34.159 happen a lot due to like millisecond
00:08:36.680 failures right where you're asserting
00:08:38.360 some time against another time there and
00:08:40.959 to be honest you probably don't care
00:08:43.159 about being that specific down to the
00:08:44.920 few like milliseconds or nanc right so
00:08:48.080 one of the things I found in my time
00:08:49.480 testing uh is that you can call on a
00:08:52.040 Time object this uh do change and then
00:08:54.560 NX zero and that will basically like set
00:08:57.399 all the msus everything down to zero so
00:08:59.680 you're never worried about that level of
00:09:02.160 granularity or if you only care about
00:09:04.839 like a date part of it and not the time
00:09:07.480 part of it um only actually assert about
00:09:10.760 the part that you care about like parse
00:09:12.320 it into a string and just assert against
00:09:14.279 the year month and day or something like
00:09:18.200 that uh this one I see bite people all
00:09:22.480 the time uh especially in controller
00:09:24.839 tests but don't assume there's going to
00:09:27.640 be an order to results if the underlying
00:09:29.920 query that gets that data for you has no
00:09:32.560 order on it um as part of my testing
00:09:35.600 Guild work at Gusto I put out a survey
00:09:38.360 at one point to all the engineers that
00:09:40.000 ask some questions about testing and one
00:09:42.640 of them was uh if you don't assert an
00:09:45.240 order for your database query will you
00:09:46.920 get it back in ID order and the answer
00:09:50.279 is no most of the time yes but you're
00:09:53.680 not guaranteed that ever so making that
00:09:56.000 assumption uh is going to lead to flakes
00:09:58.519 at some point
00:09:59.880 uh so a lot of times I'll see when
00:10:01.200 people are testing against like a
00:10:02.839 controller and they have an endpoint
00:10:04.040 that's going to return an array of
00:10:05.360 things they'll make an assertion on the
00:10:07.760 first thing and just say you know
00:10:09.079 whatever my response. first is should be
00:10:11.040 this my response. last should be this
00:10:13.200 and then eventually that's going to
00:10:14.279 flate because at some point those things
00:10:15.720 are to come back in the opposite order
00:10:18.000 here uh so do use find the array method
00:10:22.079 find to pick out the element based on
00:10:23.959 some identifier right so if you're
00:10:26.480 getting two things back from an in point
00:10:28.440 you can say find me the one that has id1
00:10:30.600 and then I'll go assert some other stuff
00:10:31.959 against it find me the one that has id2
00:10:34.160 I can go assert some other stuff against
00:10:37.279 it uh related to that don't assert order
00:10:41.200 if order doesn't matter so if you use uh
00:10:45.320 the match uh identifier for arpec and
00:10:49.000 you say you want this thing to match
00:10:50.480 another thing match is always going to
00:10:52.600 assert that those things are in the same
00:10:54.600 order here uh Alon I see you have your
00:10:57.040 hand
00:10:58.040 raised yeah I'm I'm sorry do you do you
00:11:01.120 know why um you don't get the IDS back
00:11:05.320 is it's like a like a Computing thing
00:11:08.880 like a excuse me for not explaining it
00:11:11.680 really well but um like their request
00:11:15.279 doesn't return to like postris or
00:11:17.680 whatever database you're using doesn't
00:11:19.240 like takes a little bit longer to get
00:11:20.800 certain records back and so it's not
00:11:23.560 like inter leaving the results I don't
00:11:25.680 actually know yeah in that way but yeah
00:11:29.399 so it's a it's a database slash like
00:11:31.760 active record thing I would say so so
00:11:33.480 the database is never going to guarantee
00:11:35.519 you an order unless you specifically
00:11:37.320 have an order by Clause tacked on to the
00:11:39.600 end of your SQL query an active record
00:11:42.079 will not add an order by Clause unless
00:11:44.160 you specifically put like a do order ID
00:11:47.320 ascending or something like that so like
00:11:49.200 if you aren't adding that order by
00:11:51.800 Clause uh with active record it's never
00:11:54.320 going to send one to the database and
00:11:55.720 then the database won't guarantee any
00:11:57.560 order coming back to you
00:12:02.600 does that make sense but it does it does
00:12:05.680 most of the time yeah now why it does
00:12:09.600 that most of the time I don't know like
00:12:12.880 I don't know that's okay weird database
00:12:15.720 Quirk I would say is that like as far as
00:12:17.880 I know you're not guaranteed any order
00:12:19.920 now I would say they just happen to with
00:12:21.760 active record come back in ID order most
00:12:23.720 of the time but again like there is no
00:12:26.519 guarantee for that uh Rose you got your
00:12:29.399 hand up yes I I recently learned more
00:12:32.920 about this than I thought I would ever
00:12:34.720 need to know um the query planner inside
00:12:39.120 a database I always imagine like the
00:12:41.680 query is a very simple thing where it
00:12:43.720 just like goes through the index and
00:12:46.120 grabs stuff and that's very repeatable
00:12:50.480 but the query planner is actually like
00:12:53.440 rather complex code all on its own that
00:12:56.199 is optimizing to make sure that it is
00:12:58.440 reading the data in the most optimal way
00:13:01.880 possible so the the database is actually
00:13:05.639 going based on a whole bunch of factors
00:13:08.720 that I don't fully understand what is
00:13:11.480 the optimal quickest way to get this
00:13:13.839 data for you which is why it might pull
00:13:16.519 it in different orders at different
00:13:18.079 times unless you tell
00:13:21.720 it yeah uh I could have a whole other
00:13:25.040 talk on database stuff but that's right
00:13:27.000 there and also like what the query plan
00:13:29.000 does it's based on statistics that it
00:13:30.720 calculates the time it's based on data
00:13:32.639 volume it could change based on adding
00:13:35.240 like a bunch of rows very suddenly to a
00:13:36.880 table that it could choose it have to do
00:13:38.320 something different um so that's that's
00:13:41.399 a whole other thing but like the I don't
00:13:43.360 know the tldr to takeway for this one is
00:13:45.639 like if you care about the order specify
00:13:48.920 the order on your query if you don't
00:13:51.120 care about the order don't specify it
00:13:54.040 but definitely don't get in the mix
00:13:55.399 situation where you care about it but
00:13:56.880 you did not actually specify it because
00:13:58.480 that's like guaranteed test flakiness
00:14:03.240 there cool thank you yeah thanks for
00:14:06.120 hopping in um okay yeah so still on that
00:14:09.560 topic of order match is going to assert
00:14:13.000 order every time so if you say you have
00:14:15.079 something match an array it's not only
00:14:17.120 going to care that the things you want
00:14:19.240 are in that array but that they're
00:14:20.360 specifically in that order um if you're
00:14:22.800 underlying query doesn't specify an
00:14:24.399 order again that's going to flake
00:14:25.920 sometimes so you can use match array or
00:14:28.320 contain exactly which will just assert
00:14:31.040 the membership of that array but not the
00:14:33.360 order of the elements in it uh so if you
00:14:35.560 truly don't care about the order of
00:14:36.800 something returned these are good ones
00:14:38.240 to use they will make it so your test
00:14:39.800 don't
00:14:41.880 flick uh this another one I've seen a
00:14:44.199 lot so Faker I'm pretty sure we've all
00:14:46.079 used to generate uh some data for our
00:14:49.120 tests for things we don't care about
00:14:50.600 right you often have Factory bot
00:14:52.480 factories that are using Faker under the
00:14:54.120 hood uh so you don't have to go set it
00:14:56.560 against uh all the time but there are
00:15:00.240 two problems I've seen with Faker here
00:15:02.120 so like Faker is really in the end if
00:15:04.600 you go and look at the code in GitHub
00:15:06.199 all it's doing is for something like
00:15:08.079 first name it's just got a list of a
00:15:09.880 bunch of first names and it's just
00:15:11.320 picking one at random out of there uh so
00:15:15.199 it's really important that if the data
00:15:17.360 that you're testing is important to you
00:15:19.560 like if you're assarting let's say that
00:15:21.759 the uh you have three users and you have
00:15:24.000 some method that's going to return them
00:15:25.639 by their last name in alphabetical order
00:15:29.319 right you need to be setting
00:15:30.759 specifically that last name for each of
00:15:32.800 those users and not letting Faker choose
00:15:34.560 it for you because one if you let it
00:15:37.800 choose it for you you don't know what
00:15:39.199 it's going to be that time right so
00:15:40.920 sometimes it might be I don't know like
00:15:44.000 Smith and sometimes it might be Anderson
00:15:46.759 and that's going to drastically change
00:15:48.120 the order of things returning your test
00:15:49.639 if you care about that um I've also see
00:15:52.720 it caused collisions where we had some
00:15:54.920 test flake because we're asserting uh an
00:15:57.360 order of like users returned by email
00:16:00.160 and uh sometimes the email got chosen
00:16:02.600 was actually like the same one as we had
00:16:04.360 set in another test because it is just
00:16:06.560 picking something at random so the
00:16:08.600 important thing is like if you care
00:16:10.040 about that data and you're asserting
00:16:11.319 against it in your test you need to not
00:16:12.800 let Faker choose it for
00:16:15.680 you um also setting environment
00:16:18.920 variables explicitly so when you're
00:16:21.839 running your full test Suite right
00:16:23.560 things are going tests are going to be
00:16:24.839 run in a random order assuming you have
00:16:27.160 the random seed set with our which you
00:16:29.000 definitely should uh so the best way to
00:16:32.680 get around uh having environment
00:16:34.440 variable collisions where it gets set in
00:16:36.160 one test and then a different test
00:16:37.480 expected to be somewhere else is to
00:16:39.040 always set them in round blocks so you
00:16:41.199 can grab the original value the
00:16:42.759 environment variable uh set it to the
00:16:44.920 thing you need it run your test and then
00:16:46.920 set it back to the original value so
00:16:48.560 you're basically not uh leaking state
00:16:51.279 right between one test and another sense
00:16:53.519 environment variables are Global to the
00:16:56.199 entire test context
00:17:00.160 uh this one I have seen people do if you
00:17:03.800 are testing a private method using send
00:17:06.439 this is a huge huge smell that you're
00:17:09.039 one violating the single responsibility
00:17:11.000 principle for your class and two have a
00:17:13.280 major opportunity to refactor your code
00:17:15.240 here right like if you find yourself
00:17:17.079 needing to use send to reach into the
00:17:18.959 internals of your class to test some
00:17:21.000 private method there you probably have
00:17:23.720 something that you can actually extract
00:17:25.199 out from that class right your like
00:17:28.919 public method any public method you have
00:17:30.720 on a ruby class is basically defining
00:17:32.559 what the public API is right so we care
00:17:35.280 about the behavior of the end result of
00:17:37.559 calling this thing but we shouldn't care
00:17:39.320 about the internals of How It's
00:17:41.120 implemented
00:17:43.520 um if you are reaching in to do that you
00:17:46.440 should probably be extracting that
00:17:47.880 entire method as some sort of class
00:17:50.240 right that then has a public method that
00:17:52.360 you then can test on its
00:17:55.840 own uh similarly stubbing out private
00:17:59.360 methods I've seen it test uh also a
00:18:01.880 smell here right you're now coupling
00:18:03.679 your test to the internal implementation
00:18:06.919 uh of that class which makes it harder
00:18:08.400 to refactor later right I've now
00:18:10.400 basically in my test called out some
00:18:12.840 specific name of a method that I want to
00:18:14.480 step out his behavior and so now if I
00:18:16.480 really want to like refactor that class
00:18:19.080 I can't just like go changing some of my
00:18:20.919 private methods around because I've
00:18:22.159 gotten too much into the internals there
00:18:24.720 so methods are private for a reason
00:18:27.520 right anything outside of that actual
00:18:29.760 class which includes the test testing it
00:18:32.400 shouldn't actually know about them
00:18:36.000 um so things that I can allow is like
00:18:39.720 stubbing out external calls uh a lot of
00:18:41.799 times let's say you're contacting a
00:18:43.240 thirdparty API you obviously don't want
00:18:45.760 to like do that in a real test um so
00:18:48.760 wrapping anything that talks that API in
00:18:51.240 one class and then stubbing that out in
00:18:53.080 all other tests so it doesn't do that is
00:18:55.320 fine uh that thing might be called
00:18:57.039 within a private method but it's like a
00:18:58.679 call to a different class um and then
00:19:01.720 sometimes I've seen people stub out
00:19:02.919 private methods just because they got
00:19:05.080 lazy uh creating the correct like active
00:19:07.679 record setup that would allow them to
00:19:10.120 test a certain scenario so I have
00:19:13.600 refactored some tests like that to
00:19:15.080 basically undo the stubbing to say like
00:19:17.000 okay yes it was a bit of a complicated
00:19:19.200 you know data setup we needed these
00:19:21.200 users and a bunch of this other class A
00:19:22.640 bunch of this other thing but like
00:19:23.919 that's actually representative of um
00:19:26.480 what we are trying to test here
00:19:31.039 uh this one's was definitely
00:19:32.760 controversial at
00:19:34.400 Gusto so subject by default is a keyword
00:19:38.440 in arpec uh that is described class. new
00:19:43.400 described class is the other keyword
00:19:45.039 which is literally what it says it's the
00:19:47.120 class uh of the thing that you're
00:19:49.280 testing so to me subject is the object
00:19:54.360 under test right and when people uh
00:19:58.480 refactor it and override it to actually
00:20:01.120 be like subject. do the thing that I'm
00:20:03.600 trying to do it looks very weird to me
00:20:05.880 then in the test to have some setup and
00:20:07.960 then just the word subject and then
00:20:09.400 assert a bunch of stuff because like one
00:20:12.000 subject is a noun it's not an action
00:20:14.799 right and two now I need to go look for
00:20:16.960 where you over road that to see like
00:20:18.440 what method are you even calling as part
00:20:20.120 of this thing um so personally I only
00:20:22.919 over override subject when I need to
00:20:25.480 pass some arguments to the initialized
00:20:27.400 method um so then it would just be like
00:20:30.240 subject block describe class. new with
00:20:32.440 whatever things I need to pass into
00:20:34.280 there uh I personally see this as
00:20:37.280 another sort of form of unnecessary
00:20:39.480 dryness that makes the test a little
00:20:41.520 more difficult to read and I think also
00:20:44.520 I've seen more Junior developers kind of
00:20:46.360 struggle with that too that like
00:20:48.000 sometimes subject is the class an
00:20:50.039 instance of the class under test and
00:20:52.039 sometimes it's me taking an action on
00:20:54.080 that instead of class under test which
00:20:55.679 makes it a little
00:20:57.120 confusing
00:20:59.960 uh similarly so we talked about the 3as
00:21:03.320 earlier um it's pretty common in arsp to
00:21:06.799 assert that a some instance of a class
00:21:10.440 or something receives some method with
00:21:12.279 some arguments right um you do that in
00:21:16.080 order to sort of isolate your class
00:21:18.240 under test and not always have to call
00:21:19.840 through a bunch of different classes on
00:21:21.279 there but I I even strangled with this
00:21:24.559 many times where there is a format where
00:21:26.360 you can say expect thing to receive some
00:21:30.120 method call and then I act on my thing
00:21:32.720 and that's it that's the test so it's
00:21:34.480 now like act and then assert uh or sorry
00:21:39.400 assert and then act which is weird so I
00:21:42.600 always do the more verbose format of
00:21:45.840 this which is allow my thing to receive
00:21:48.960 the method I care about then do the
00:21:51.200 thing and then expect my thing to have
00:21:54.200 received that method that I care about
00:21:55.960 which keeps your arrange act assert
00:21:59.320 format and it's a lot easier to sort of
00:22:01.480 wrap your head around I'm like positive
00:22:03.679 we've all been there as a developer
00:22:05.559 where it's not in that format and do the
00:22:07.720 expectation first and you get very
00:22:09.760 confused as to why your thing is like
00:22:11.279 not passing over and over again before
00:22:13.000 you realize it's because you have the
00:22:14.159 thing in the wrong order um so it's
00:22:16.679 three lines instead of two but
00:22:18.960 personally I think this is way easier to
00:22:20.559 actually follow and then you never break
00:22:22.120 that arrange acert pattern in your
00:22:26.840 test uh
00:22:28.960 this one's really just for readability
00:22:31.240 here and kind of focusing on the like um
00:22:34.880 how do I sort of keep my test small and
00:22:37.440 isolated um rspec has some cool uh
00:22:41.279 matchers so anything is one of them
00:22:43.840 which is really like you can say if you
00:22:46.240 have a hash you have a key and then the
00:22:48.080 value is just anything if I don't care
00:22:50.000 about that value in my hash that's not
00:22:51.640 the thing I'm testing against right now
00:22:53.799 um you can also specify parts of hashes
00:22:56.039 by saying like expect this thing to be a
00:22:58.440 hash including some key I care about
00:23:00.720 some key some value I care about and
00:23:02.880 ignore the other like 10 keys and values
00:23:05.320 that are in there um I tend to find it's
00:23:07.600 a lot less readable when people say like
00:23:09.760 expect this response to be some giant
00:23:11.720 blob of a bunch of uh like you know 10
00:23:14.480 different keys and values when really
00:23:16.559 all I'm trying to care about is like two
00:23:18.279 or three of them in there for the
00:23:19.559 purpose of this test so I tend to really
00:23:22.480 enjoy using rspc sort of like
00:23:24.200 composability of a bunch of different
00:23:26.279 matchers that they have in here to
00:23:27.720 really like make sure to focus on what
00:23:30.279 I'm actually trying to test in that
00:23:31.720 specific
00:23:33.600 test
00:23:35.279 um this one is a pet peeve of mine I
00:23:38.679 don't like it blocks without a title um
00:23:42.080 tests are documentation right uh they
00:23:45.720 are documentation for you and your
00:23:48.799 viewer and your future self a year from
00:23:51.799 now who doesn't remember why you wrote
00:23:53.440 this test and you get blame your code
00:23:55.240 and realize it was you who did this
00:23:56.760 thing um and other future developers who
00:23:59.039 are going to work at your company uh
00:24:02.600 especially coming from a place like
00:24:03.760 Gusto where we have 400 plus people
00:24:05.960 working on like one rails project it's
00:24:08.480 really important that you leave good
00:24:10.919 documentation for everybody right so uh
00:24:15.440 I tend to find when people just have an
00:24:16.679 I block without a title like that's not
00:24:18.159 helpful to me when I go to look at the
00:24:20.159 test file to read through like what is
00:24:22.120 this class actually supposed to do now I
00:24:24.120 sort of need to like make inferences and
00:24:26.240 whatnot instead of having a nice I don't
00:24:28.159 know if anybody uses Ruby mine but you
00:24:29.679 can sort of collapse everything down and
00:24:31.159 then just start breaking out ITP blocks
00:24:33.039 and like opening them back up it's
00:24:34.200 really easy to just read through uh like
00:24:36.559 all the different things that this class
00:24:38.720 is supposed to do and if you don't
00:24:40.520 specify a title that says exactly what
00:24:42.520 you're doing it's harder to
00:24:45.960 follow uh another source of flakiness um
00:24:49.159 don't specify specific IDs for active
00:24:51.240 record objects uh talking about the
00:24:53.520 database level for every table you
00:24:56.080 create there's a sequence that goes
00:24:58.039 along with it that keeps track of what
00:24:59.520 the next ID should be there um you don't
00:25:02.679 need to specify certain IDs sort of
00:25:04.799 similar to before like your test
00:25:06.320 database is going to get wiped in
00:25:07.640 between runs but it's very possible that
00:25:10.240 at some point you end out with a certain
00:25:12.679 test run where that ID of 15 that you
00:25:15.200 specified directly for some active
00:25:16.640 record object actually gets taken before
00:25:18.799 that and then everything starts failing
00:25:20.799 so like just always allow the ID uh the
00:25:23.360 sequence to set the ID for you and then
00:25:25.760 if you need to reference it for a
00:25:28.080 different object right like you're
00:25:29.320 trying to tie two objects together need
00:25:31.600 one uh you reference it as variable. ID
00:25:34.600 and you never have to know exactly what
00:25:36.080 that ID
00:25:38.559 is uh this is another smell one for me
00:25:42.559 so if you find yourself writing out your
00:25:45.640 it statement block and your title here
00:25:48.080 and you're saying it does this and this
00:25:51.720 other thing on there then in general
00:25:54.440 that's kind of also a smell that like
00:25:56.440 you're testing too many things things in
00:25:58.080 that one test right uh I love to use
00:26:01.440 context blocks to break those up so you
00:26:04.880 know you have a context that says when
00:26:07.480 you're an admin it does this thing when
00:26:10.720 you're this type of user it does this
00:26:12.720 other thing um I think you can also like
00:26:17.399 uh makes the title shorter and more to
00:26:19.159 the point too but uh yeah I take this as
00:26:22.279 a smell if I catch myself writing some
00:26:24.480 sort of conjunction in there that it's
00:26:26.760 probably actually two tests or more that
00:26:28.799 I need to be breaking out
00:26:32.080 here um don't use class variables this
00:26:35.360 one's kind of a testing one but just one
00:26:37.799 in general so class variables uh sort of
00:26:41.080 like environment variables are Global to
00:26:43.919 the context of that class uh which means
00:26:47.559 that every instance of that class shares
00:26:50.120 that one class variable so if it changes
00:26:52.840 in one instance it changes for
00:26:54.679 everything um my good story about this
00:26:57.679 is the very first job I had as a junior
00:27:00.520 developer out of Turing uh which is a
00:27:02.640 coding school here in Denver uh we were
00:27:05.960 doing uh hotel bookings right for people
00:27:09.039 uh so we're actually like making calls
00:27:10.760 to various agencies like Expedia and
00:27:12.760 stuff to book real hotel rooms and
00:27:14.679 charge people's real credit cards and
00:27:16.600 whatnot and in one of our classes
00:27:19.399 responsible for the booking stuff there
00:27:21.440 was some class variable that controlled
00:27:23.600 whether or not this was like do this
00:27:25.760 thing for real and production or like
00:27:27.679 okay it's testing we're going to stub it
00:27:28.960 out and there was just this giant
00:27:30.240 comment block that just said like never
00:27:32.600 ever change this if you're going to test
00:27:34.440 on here you're actually going to be
00:27:35.840 trying to book hotel rooms from running
00:27:37.760 the test Suite um which was terrifying
00:27:40.640 as a junior developer but also like itly
00:27:44.240 unnecessary uh so go back to this yeah
00:27:47.240 like use environment variables that you
00:27:48.919 can stub out and reset in context blocks
00:27:51.840 there it's a lot easier to reason about
00:27:55.640 too uh yeah and so that's it I've got
00:27:58.640 some other resources in here uh this was
00:28:01.320 actually a slightly shortened version of
00:28:02.799 this presentation because last time I
00:28:04.799 did it we got into way more discussion
00:28:06.360 and it took like two hours um so I can
00:28:09.640 probably post these links somewhere um
00:28:12.240 but if you want to look at this one I
00:28:14.279 think this Ruby com talk uh from a few
00:28:16.360 years ago when I was there is actually
00:28:17.880 it's on like non-deterministic specs and
00:28:20.440 everything it's one of the like better
00:28:22.880 Ruby comp talks that really walks you
00:28:24.559 through like all the different ways you
00:28:26.000 can have non-determinism and how to fix
00:28:27.559 them and whatnot so like highly highly
00:28:29.480 recommend watching that one
Explore all talks recorded at WNB.rb Meetup
+21