How I Learned to Stop Worrying and Love Unit Testing 
					
						Hello and welcome to my talk on unit testing. I'm Valerie Woolard
						Srinivasan, I'm a software engineer at Panoply, where I help build
						tools for your favorite podcasts. Find me in the hallway later or
						on Twitter, my handle is valeriecodes, for podcast recommendations,
						but right now we're going to talk about testing.
						Before we get into the talk, I also want to take a moment to
						appreciate our location in the beautiful city of New Orleans and to
						borrow an Austrialian tradition called the acknowledgement of country,
						which is something I first saw Pat Allen do at Nation Ruby, to
						acknowledge the native people who first lived on this land.
						I take time to acknowledge the Choctaw, Houma, and other tribes, the
						traditional custodians of this land, and pay respects to their elders
						past and present, and I extend that respect to other indigenous people
						who are present.
						Thank you for taking the time to appreciate their contributions to
						this land and this culture with me, and for coming to attend this talk.
						Let's get started.
						I'm happy to be kicking off the testing track, I hope that this can
						teach you something if you're already doing testing and to give you
						an idea of where to start if it's something new to you.
						I chose this gif of Kermit the frog, partly because I love Kermit
						the frog and it made me laugh.
						But the other reason is that I love the carefree nature shown here.
						I think that a lot of the power of testing is confidence. Good tests
						allow you to be confident that you won't accidentally break something.
						Good tests allow you to change code and verify that it works the same
						way it did before. Good tests let you worry less. Maybe they don't let
						you be as care-free as Kermit, but they come close.
					 
				 
				
					What are tests for? 
					 
					
						Let's start right off the bat with: What are tests even for in the
						first place? Some of you may be fairly new to software and just know
						that testing is probably something you should do without knowing why.
						I talked a little bit about confidence, and one way you can think
						about tests is as an act of kindness. When you write a test, you are
						taking the time to verify that your code works. This is a favor to
						your teammates and your future self. Think of it as an investment.
						Coming into a new codebase or starting on a new project often feels
						familiar in certain ways, it may be a language you're familiar with
						or a type of application that you've built before, but there are gaps,
						like playing croquet with a flamingo and a hedgehog. Your old
						assumptions may not hold true, and you have to figure out where the
						gaps are between your understanding and reality.
						Tests help bridge that gap.
					 
				 
				
					Who are tests for? 
					
						To all viewers but yourself, what matters is the product: the finished artwork. To you, and you alone, what matters is the process
					 
					— David Bayles & Ted Orland, Art & Fear  
					
						Let's also take a moment to talk about who your tests are for.
						There's a bit of a story behind this quote. My parents are both in the art
						world, and got me this book written by a friend of theirs about being
						a professional artist. I'm pretty sure they were hoping I'd be a
						professional artist. I'm not. But I really love this book, called
						Art & Fear, because I think that it's incredbily relevant to
						programming and all sorts of creative work.
						I'm really obsessed with it, and this will not be that last time I
						quote it in this talk. I have an idea for this talk where I just
						lecture about this book in the context of programming, but that's
						for another day, I just had to sneak it in somehow.
						"To all viewers but yourself, what matters is the product: The
						finished artwork. To you and you alone, what matters is the process"
						In the context of software, the process of creating your artwork is a
						collaborative one, so it's definitely a little different than being a
						solo artist, unless you're just working on a side project by yourself.
						That said, writing tests is part of the proccess of writing software,
						not the finished product. A user of your application doesn't care
						about your tests as long as the product works. You should care about
						writing tests because they will help you to build a product that works.
						If you're a user, you'd probably rather use a product that works perfectly
						and has no tests, but as a software developer you'd much rather use a
						product that has some issues but is well-tested.
					 
				 
				
					Protecting against bugs 
					 
					
						The first and perhaps most obvious function of tests is protecting against
						bugs, or at least catching those bugs before they make their way to
						production.
						The software that you're working on is likely to be a very complex
						system. You're probably not going to be the only one modifying it,
						and it's not going to be possible to understand or keep track of
						what's going on in every part of the application.
						A test is an audit trail of how something is supposed to work. When
						a test fails, you should have some proof or paper trail of when that
						thing used to work so that you have an easier time identifying what
						broke it.
						Use tests to prove that your code does what you say it does when you
						write it.
						Ideally, you use a continous integration proccess to test your code
						every time there is a new commit. Good tests coupled with continuous
						integration will allow you to pinpoint and quickly correct any code
						that breaks your tests.
					 
				 
				
					Documentation 
					
						
							
def number
 # always returns 4
 4
end
							 
						 
					 
					
						
def number
 # always returns 4
 5
end
						 
					 
					
						
it 'always returns 4' do
 expect(number).to eq(4)
end
						 
					 
					
						Another great asset that tests can provide for you is documentation.
						You may be more familiar with writing documentation in the form of
						comments or internal wikis, but that can get out of date quickly
						and cause trouble. For example, here I've written a method that
						returns 4 and a comment explaining it. However, if I change the
						return value of the method, the comment remains, as a terrible,
						blatant lie.
						If you're using continuous integration, tests have to be updated
						when they fail, so they are more likely to reflect the current
						state of the codebase than comments.
						In the above example, nothing forces the comment to be updated when the
						output of the function is actually switched to five. In a less trivial
						case, this could lead to comments giving future developers (including
						yourself!) misleading information about the actual state of the code.
						Good tests can serve as an introduction to your code. Well-written tests,
						along with well-named functions and variables,should explain the
						desired behavior of your code as well as testing its functionality.
					 
				 
				
					Encouraging good practices 
					Tests are the canary in the coal mine; when the design is bad, testing is hard. 
					— Sandi Metz, Practical Object-Oriented Design in Ruby  
					
						Tests can
						help prompt you when your code is getting too complex and help
						encourage good practices. As Sandi Metz said in Practical Object-Oriented
						Design in Ruby, "Tests are the canary in the coal mine; when design is bad,
						testing is hard."
						Writing tests as you write your code will help give you an idea of
						where the complexities in your code are. If you are not sure how to
						test a method, that method might be too complex and need to be
						broken up into smaller functionality.
						If you find yourself having to write lots of scaffolding in order
						to even run your tests, this can help to signal that the class you're
						working with might have too many external dependencies.
						The Law of Demeter in object oriented programming states that each
						unit should only have limited knowledge of other units, and that a
						unit should only talk to its immediate friends. Writing unit tests
						provides a great opportunity to test this. How much does your class
						actually need from neighboring classes? If it's too much, then writing
						unit tests will be more complicated. This is a hint to you to go back
						and simplify the code that you're testing.
					 
				 
				
					Allowing for confidence in refactor 
					 
					
						Allowing for confidence in refactor. One great thing about testing
						is that it makes refactoring possible. If you've got some code that's
						written in a way that makes it very difficult to reason about, you
						can't rewrite it if you can't reason about it in the first place.
						How are you going to preserve all that functionality without knowing
						what it is?
						Without tests, it's impossible to make changes to your code and
						ensure that you haven't broken other things. You are, indeed, living
						very dangerously, because your only conception of how the code is
						supposed to work is within your own head, and you have no way of
						verifying that this lines up with how it actually works or how it was
						orignally intended to work without tests.
						Efficient refactoring is only possible with a well-written test suite.
						This is an example of how taking the time to write good tests will
						save you time in the future. Refactoring is a hugely important part
						of writing good code, just as revision is an important part of writing.
						Remember in high school when you were writing an essay, and as soon as
						you got to the right word count you just hit print and handed it in.
						I was always terrified to re-read them because I was worried I'd
						find a mistake and go back and have to do a bunch of work.
						This isn't really a sustainable way to work, especially in the
						context of a code base. Writing code without tests is a lot like
						this. You're not only depriving yourself of the chance to introspect
						a bit more about the code you've just written while it's fresh in
						your mind, you're also depriving future developers or more insight
						into how your code is supposed to work and how to tell if it is
						working. This knowledge is essential to making changes later.
					 
				 
				
					
						You make good work by (among other things) making lots of work that
						isn't very good, and gradually weeding out the parts that aren't good
					 
					— David Bayles & Ted Orland, Art & Fear  
					
						Here's another quote from Art & Fear. This quote struck me in the
						context of testing, because I feel like it's also kind of about refactoring.
						"You make good work by (among other things) making lots of work that
						isn't very good, and gradually weeding out the parts that aren't good"
						I think this is absolutely true about programming as well. I do most
						of my work by just putting my fingers to the keyboard and reasoning
						out the problem at hand in whatever manner first comes to me.
						This is often pretty repetitive and sometimes needlessly complicated,
						but this rough draft and starting point is essential to growth. Being
						able to edit and make improvments to your code over time is an essential
						part of improving as a developer.
						But you can't remove the cruft without understanding exactly how
						your code is supposed to work and--crucially--when it stops working.
						That's the insight that tests give you.
						Tests are your first chance to introspect about your code. When you
						write tests, that gives you a chance to look through your code to
						get a sense of how you feel about it. Are the variables well-named?
						Is the logic clear? Do you feel like you're doing something hacky?
						Do a gut check and make changes where you see fit. After all, you've
						got some tests now so that shouldn't feel quite as scary.
					 
				 
				
					A Good Test Suite 
					
						Fails when something is broken 
						Doesn't fail otherwise 
					 
					
						These are the things tests can do for you, but what makes a good
						test? Obviously not all tests are created equal, so what
						differentiates them?
						A good test suite really only has to do two things. It needs to
						break when code that you've changed has broken your app, and not
						break otherwise. Both of these things are much easier said than
						done. As it turns out, it's very difficult to predict what parts
						of an application are most likely to break with future changes,
						and write tests in a way that exposes those changes.
						It's also quite easy to write tests that break in ways that don't
						actually indicate failures in your code, like a copy change or a
						change in the formatting of an output. You should also be wary of
						tests that might be prone to timeout or assume anything that could
						change, such as the year or anything about the environment it's
						being run in.
						
					 
				 
				
					Different types of tests 
					
						Unit 
						Integration 
						Functional 
						System 
						
							So this talk is about unit testing, but we haven't really
							differentiated the different types of tests and what they mean.
							You'll hear slightly different definitions from different sources.
							These different designations are used in the Ruby on Rails guides.
							There are unit, functional, integration, and system tests.
							A unit test tests the smallest functional unit of code that it can,
							such as a single method of a single class.
							An integration test tests aspects of a particular workflow, and
							thus tests multiple modules and units of your software at once.
							You might create an integration test to make sure that users can
							log in or create accounts.
							Functional tests look at controller logic and interactions. They
							are testing that your application handles and responds to requests
							correctly.
							System tests allows test user interactions with your application,
							running tests in either a real or a headless browser.
							System tests are like an automated QA script, and probably most
							closely mirror the way you would perform manual QA in an automated
							environment.
						 
					 
				 
				
					Why unit tests? 
					
						Easy to write 
						Easy to run 
						Easy to reason about 
						Encourage simplicity 
					 
					
						Of the types of tests I talked about, you'll notice that unit tests
						are by far the simplest.
						I like them because they are easy to write, easy to run, easy to
						reason about, and also help to encourage modularity and cleaniness
						in your code as we discussed earlier.
						Individual methods are probably the things in your code that you
						have the best understanding of, making them the easiest to write
						good tests for. And if your code is clean and modular, well-tested
						units should lead to a functional application.
						Unit tests are also very fast to run, since they have the fewest
						dependencies and don't require spinning up a headless browser.
					 
				 
				
					When to write other tests 
					
						That said, there will be times when you have to write other types of
						tests, but you should be thoughtful about when those times are because
						of the overhead involved. It's probably a good idea to have integration
						tests for the most critical workflows in your app, or in the case of a
						company, the things that would be most likely to loose you money quickly
						if they broke. System tests can be used to test important workflows,
						but can be slow and brittle, so should be used with caution to
						supplement a robust unit test suite.
					 
				 
				
					Some things you can test 
					
						Return values of a method 
						 expect(2 + 2).to eq(4) 
						Whether other methods are called 
						Whether a job is enqueued 
						If something is truthy/falsey 
						If an exception is thrown 
						 
					
					
						Here are some things that you can check for in your tests. This is
						by no means an exhaustive list, and there's plenty of documentation
						to be found online on how to test different things.
						The most important thing to keep in mind as you start testing is to
						keep things simple. Each test should only look at one very small
						thing.
						Each of the bullet points I've listed above, and the most common one,
						at least for me, I've given an example of. They're examples of what
						are called assertions. You are establishing an idea of what your code
						should do, and when you run the test the computer will tell you if
						it actually does it or not.
						I've added in an example of using RSpec to run a few tests in just
						a simple irb console. You can see that when I run the first test, it
						passes, and when I change the return value, the test fails, so the
						failure message includes the expectation that was not met, the
						expected value and the actual value.
						For example, you can test the exact value of a return value.
						Incidentally you can also use array matching, greater than, less
						than, includes, the whole deal. You can make sure that a method
						causes another method to be called, you can check that a job is
						enqueued, you can check that a database object is created or
						destroyed, you can see if something is truthy or falsey, or whether
						running a particular bit of code throws and exception and the
						content of that exception.
						This is by no means an exhaustive list, but instead meant to get you
						thinking about how you might write unit tests for your methods.
					 
				 
				
					What is a unit test? 
					class Person
  def greet(name)
    name.instance_of?(String) ? "Hi #{name}!" : "Hi! Didn't catch your name."
  end
end
describe '#greet' do
  it 'contructs greeting based on string passed as name' do
    expect(Person.new.greet('Valerie')).to eq('Hello Valerie!')
  end
  it 'doesn\'t catch names for numbers' do
    expect(Person.new.greet(2)).to eq('Hi! Didn\'t catch your name.')
  end
end 
					
						What, exactly, is a unit test.
						As we noted, a unit test looks at the smallest possible unit of code. In the case
						of Ruby, that's a single method on a single class. In unit testing a method,
						you should think about all the reasonable inputs to that function, as well as
						how the method should respond to invalid inputs. If your method uses branching
						or conditional logic, you should have a unit test that hits each possible
						branch or combination of branches.
						Here's an example of how I might approach unit testing this method that I
						wrote called "greet."
						It takes someone's name and says hello to them. If it
						gets a weird input, like a number or anything that's not a string, it just
						says hello and mentions it didn't catch the name. That's a design decision
						on my part, I could also throw an exception or just call to_s no matter what.
						The code examples here use rspec, but the general ideas should be applicable
						to whatever testing framework you're using.
						Because I have two possible conditions in my return values, I need at least
						two unit tests. If I wanted to be especially thorough, I might test for other
						edge cases, like different types of non-string input. You can see how if you
						have lots of branching logic this can multiply and get complicated quickly,
						so let that serve as yet another incentive not to nest too many conditionals
						in a single method. I've written a simple test for each of the possible
						conditions in this case.
						I've written two unit tests, one that calls the method using an
						expected input... this is something you might see called a happy
						path. I call the method using my name, and it says "Hi Valerie!"
						I apologize for having to use a ternary operator here, which I did
						so I could fit everything on the slide, but the basic idea is that
						it's just an if/else statement with the if part behind the question
						mark, the return value for if after that, and the return value for
						the else clause after the colon.
						My test is looking to make sure that the method does in fact return
						"Hi Valerie!" when I call the method using my name.
						I also have a test for the else case in which I pass a number and make
						sure I get the message indicating that it didn't catch my name.
						The site betterspecs has tons of resources on the style in which you can
						write your test descriptions and structure your tests.
						The same principles of readability that apply to your code are probably
						even more important in your test. An English speaker who doesn't know
						Ruby should be able to read your test and have an idea of what it is
						doing. In RSpec, things are named very deliberately to allow for this.
						And the less readable your code is, the more straightforward your tests
						should be, although both should be as straightforward as possible.
					 
				 
				
					What makes testing hard? 
					
						So we've talked about what's good about tests and why you should write
						them, but we've probably all been or will be in situations where
						test-writing is skipped or overlooked. Why is that? What is it that
						makes testing hard or causes it to be passed over?
					 
				 
				
					Time 
					
						Developer time 
						Computational time 
						The passage of time 
					 
					
						A lot of these challenges boil down to time. Developer time,
						computational time, and the passage of time.
						Writing tests takes time. The first time you're writing something,
						you're probably testing it manually in a development environment.
						You've already convinced yourself that it works, and it seems
						straightforward enough. Why write automated tests? That takes time
						that you could be spending writing your next feature.
						Tests also take time to run. By configuring a continous deployment
						environment in which you have to run tests before you deploy new code,
						in one deliberate way you are slowing down your deploy process. This
						can make it more frustrating to push urgent fixes through, especially
						if those tests take a long time to run or have spurious failures.
						You can also mitigate this by writing fast tests (such as unit tests
						over integration tests), keeping those tests simple, and using tools
						like Zeus to load tests faster.
						Testing anything that involves time can also be challenging. What
						if you want to check that a time stamp was correctly recorded, but
						fractions of a second elapse between the time the object is saved and
						the test is run? What if your test server is on a different time
						zone than your development and production environments?
						These issues can be mitigated by using gems like Timecop, which
						allow you to freeze time in your testing environment.
						The final aspect of time that can be difficult is knowing at what
						point in the process to write your tests, and if you're waiting to
						write them until the end or trying to write them long after writing
						the code under test, it can be hard to remember what you were doing
						an what things are most important to test.
						We tend to overestimate our own abilities to remember things, and you
						are likely to forget the context for your code and decisions very soon
						after you're done writing.
						Even if you're not using true test-driven development, you should be
						writing tests alongside your code, and writing out an idea of what
						you want your tests to look like before you begin writing code can be
						a helpful exercise in thinking about how to structure your code.
					 
				 
				
					Not knowing where to start 
					
						If you're faced with an app or codebase with no tests, it's daunting
						to try and figure out where to even start testing it.
						Instead of taking on the monumental task of writing tons of tests
						at once, instead make sure that every new piece of code that you add
						is well-tested. Choose a testing framework and use tools to get an
						idea of your test coverage. Chip away at it. Use the Boy Scout rule
						and leave the code cleaner and better-tested than you found it with
						every pull request.
						Are you fixing a bug? Stop. Before you fix it, write a test that
						exposes it and fails. Now go fix the code and turn it green.
						Are you writing a method? Make sure that you have a test (a separate
						test) for every branch of its conditionals. Think about edge cases
						(the method gets passed a nil value, a string instead of a number,
						a negative number, zero, a really big number). Think about the ways
						you want that method to behave in those conditions and write tests
						to validate that behavior.
						Favor tests over comments as a means of explaining your code to
						future engineers.
						Get everyone on your team to agree that testing is important, agree
						on a strategy. You can use tools like coveralls to give you feedback
						about what code is and isn't covered by your test suite. Once you have
						a starting point, you can set a goal, for example to increase code
						coverage by 5%. You can decide that all PRs need to include tests in
						order to be merged.
					 
				 
				
					External systems 
					
						Let's say that your code makes a call to an external API and then
						parses that response. It's inefficient and clunky to actually make
						that call every time you run your test, so what can you do?
						You have a few options. You can mock out a valid response in your
						tests and just make sure that you are doing the correct operations
						on it.
						You can use a tool like Webmock to construct fake HTTP responses,
						or a tool like VCR to make a real request once and record the result,
						performing all future tests from the recording.
						Keep in mind that both of these methods rely on the current state or
						your interpretation of the current state of the API response format,
						and that if that changes it may break your live app but not your tests.
					 
				 
				
					Go forth and conquer 
					
						I hope this talk has given you some ideas for how to start testing
						if you haven't already, and has given you some ideas and things to
						think about in terms of your test suite if it's something you're
						doing already.
						Feel free to find me later or on Twitter with any questions, and if
						you're interested in working for my company, Panoply, we are hiring
						and I'd be happy to chat about that as well.
						Go forth and conquer!
					 
				 
				
				@valeriecodes