Hello, world
Last updated
Last updated
It is traditional for your first program in a new language to be . So, let's do it!
Open your terminal and create a hello
directory:
Open your favorite editor, create a new file called hello.rb
and put the following code inside it:
To run it type ruby hello.rb
in your terminal and hit <Enter>
. Then you should see the string Hello, World
being printed in your screen.
The puts
instruction stands for "Put String", and what it does is to put the string you give to it in the output.
In our code we're giving the "Hello, World" string.
How do you test this program?
The answer for this question is our first example of how TDD promotes software design.
To make our program testable it is good to separate our "" code from the outside world (side-). In our program the greeting string is our domain, and the puts
is a side effect (printing to the standard output).
Let's separate these concerns so we can easily test our domain's code.
As we want to use Object-Oriented Design, we're going to create a class named Greeter
with a method named #hello
to generate the string with the greeting. Now our code should look like this:
The method simply returns the string Hello, World
. Then we created an instance of Greeter
and invoked the hello
method, passing the results to puts
.
I'd like to call your attention to the fact that the code above has a lot of parentheses. If you're coming from another programming language you may think it's perfectly normal.
In Ruby the use of these parentheses are usually optional. They may be required to clear up what may otherwise be ambiguous in the syntax, but in simple cases the Ruby programmers usually omit them.
Let's code like a real Rubyist and clear the unnecessary parentheses from our code:
Another Ruby syntactic is that the last evaluated expression of a method is always returned. Therefore the use of the return
in the last line of that method is unnecessary.
So, let's clear our code a little more:
That's much easier on the eyes, don't you agree?
As mentioned before, we created a class to achieve a separation of concerns. It was created specifically to handle the greeting string, but how this string is going to be used is not the class's concern.
A common Rubyist practice is to put classes in their own file and name it after the class but using snake_case
. Then let's create our greeter.rb
:
As our class is now in a different file, the hello.rb
needs to require that file to have access to the Greeter class.
Here's how the hello.rb
must look like:
Just to check if everything is fine, type ruby hello.rb
in your terminal again. You should still see the string Hello, World
being printed in your screen.
Now create a new file called greeter_test.rb
where we are going to write tests for our Greeter
class.
Don't worry if you don't understand everything. After writing and running this test file I'll explain this code.
The next step is to run the test.
Enter ruby greeter_test.rb
in your terminal and you should see something like this:
If you see an output similar to this 👆 then your test passed!
Writing a test with Minitest is like writing a method in a class, but with some "rules" (some of them are not actual strict rules)
The file should be named like *_test.rb
(not mandatory, but a good practice)
The file must require 'minitest/autorun'
so we can run the tests from the command line.
The file must require_relative 'greeter'
so it can access the code being tested.
The test must be written in a subclass of Minitest::Test
(don't worry if you still don't know what a subclass is)
The test method must start with test_
The assertion determines if the test is to be considered successful or not. In this case we are asserting that the expected value is equal to the actual value using assert_equal expected, actual
.
In our greeter_test.rb
code we're covering some new Ruby concepts:
When a file needs the code defined in another file, a way to access it is requiring the "another file".
When we use require
, we're getting the code from a package installed in our system (in Ruby we call such packages as "gems").
When we use require_relative
, we're getting the code from a file stored in a path relative to the current file. In our case, as both greeter_test.rb
and greeter.rb
are in the same directory, we can simply use require_relative 'greeter'
(the .rb
extension can be omitted).
In the code below we're creating a new class named TestGreeter
as a subclass of Minitest::Test
:
The concept of class and subclass will be explained in another moment. For now just keep in mind that when we create the TestGreeter
as a subclass of Minitest::Test
, this means that TestGreeter
inherits the behavior defined in Minitest::Test
.
This is the method where we are actually testing our hello method:
We create a greeter object, then call the hello
method, then define the expected result, and then finally assert if the expected result is equal to the actual result.
When we ran this test it passed, which is nice. Now let's move on...
In the last example, we wrote the test after the code had been written. We did this so that you could get an example of how to write a class, a method and create a test for it. Now we're going to delete that test we just wrote a start a fresh one. From this point on, we will be writing tests first, and then the implementation (usually called production code).
This is basic test-driven development and allows us to make sure our test is truly testing what we want. When you retrospectively write tests, there is the risk that your test may continue to pass even if the code doesn't work as intended.
Our next requirement is to specify the recipient of the greeting. Let's start by capturing these requirements in a test (remember to delete the previous one). The greeter_test.rb
now looks like this:
Now run ruby greeter_test.rb
and you should see an error like this:
It's important to pay attention to the error message here, it gives us the information we need to figure out what's wrong with our code.
The Ruby interpreter is telling what you need to do to continue. In our test we passed an argument to the #hello
method, but it is not prepared to receive an argument. That's why Ruby is telling us: wrong number of arguments (given 1, expected 0)
.
Edit the #hello
to accept an argument. Now your greeter.rb
should look like this:
If you try and run your tests again, you should see a failure with this message:
The test is now failing because it's not meeting our requirements.
Let's make the test pass by concatenating the name
we passed as argument to the string we want to return.
When you run the tests now, it should pass.
Normally, as part of the TDD cycle, we should now refactor.
At this point, we have working software backed by a test. It's a good time to commit our code:
Don't push to main
though, we are going to refactor next. It is nice to commit at this point in case you somehow get into a mess with refactoring - you can always go back to the working version.
The string concatenation is working fine, but the usual way to achieve this kind of result is by interpolation. In Ruby, we do this with #{variable_name}
, like here:
Run the test and it should pass.
The next requirement is when our method is called with no arguments, it defaults to printing Hello, World
, rather than Hello,
.
As TDD practitioners, we write the tests first, so let's write a new failing test.
Do note that the test method now have a descriptive (and long) name. It's important to give descriptive names to your tests, so you can know what to do when they fail.
After running the tests we'll see an error message like this:
That error message is telling us that:
the TestGreeter#test_say_hello_world_when_called_with_no_args
failed
it failed due to the ArgumentError
, because the #hello
expected 1 argument and we didn't give any.
Let's check our #hello
again:
Now we have a dilema:
#hello
expects an argument so we can use hello("John")
we also want to be able to call #hello
with no arguments and it should respond with "Hello, World"
To solve this we can define a default value for the name
argument, like this:
As we're defining a default value for the name
variable, calling #hello
with an argument is optional. Because if none is passed, it uses the default value.
Run the tests and you should see a successful run. It satisfies the new requirement and we haven't accidentally broken the other functionality.
It is important that your tests are clear specifications of what the code needs to do.
Now that we are happy with the code, let's amend the previous commit to check in the new version of our code with its test.
Example:
Let's go over the cycle again:
Write a test
Run the test, see it fails and check the error message
Write enough code to make the test pass
Refactor
On the face of it this may seem tedious, but sticking to the feedback loop is important.
Not only does it ensure that you have relevant tests, it helps ensure you design good software by refactoring with the safety of tests.
Seeing the test fail is an important check because it also lets you see what the error message looks like. As a developer it can be very hard to work with a codebase when failing tests do not give a clear idea as to what the problem is.
By ensuring your tests are fast and setting up your tools so that running tests is simple, you can get in to a state of flow when writing your code.
By not writing tests, you are committing to manually checking your code by running your software, which breaks your state of flow. You won't be saving yourself any time, especially in the long run.
A new requirement arrived! The greeter needs to be able to greet people in a different language.
We're going to achieve this by defining the language when we create the greeter object, passing the language to the constructor method.
Of course, we'll use TDD to flesh out this functionality.
Let's write a test creating a Spanish greeter and calling the #hello
.
Remember not to cheat! Test first.
We can see that our first two tests are still passing, but the new one is getting an error: ArgumentError: wrong number of arguments (given 1, expected 0)
.
A curious thing is that the error message says the problem happened in the initialize
method. This happens because in Ruby, when we create an object with #new
, it invokes the #initialize
method. By default #initialize
doesn't do anything, but we can override this behavior by defining it in our class. Then, let's do it!
Put the #initialize
method as the first method of your class:
Here we're defining an instance variable named @language
. Any variable prefixed with an @
symbol is considered an instance variable. It belongs to an specific instance of the class and can be accessed in any method.
Our constructor method is assigning the value passed as argument to the @language
instance variable.
Now let's run the test and check the output:
😱 Our change broke all tests!!!
Always keep this in mind: if your change breaks tests that are unrelated to your current work, you're probably doing something wrong!
Tests are a safety net that brings confidence to change the code with no fear. If tests fail because you've broken the code, the cure is simple: undo the last change and make a better one.
In our case here we're breaking the previous tests because we added a new mandatory argument to the #new
method: language
. In order to fix this we should make it optional by setting a default value for it.
Let's run the tests:
Good, now we have a failing test output with a clear direction about what must be done to make it pass. It's expecting a greeting with Hola
and our code is greeting with Hello
, so let's fix our #hello
:
Run the tests and you'll see it pass. Time for refactoring.
For now, I decided to make no changes, then let's move on.
Let's add a test for the French language, by following the steps:
Write a test asserting that if you pass in "french"
you get "Bonjour, "
See it fail, check the error message
Do the smallest reasonable change in the code
The test will fail with:
Then let's write enough code to make the test pass.
Run the tests and you'll see it pass.
Time for refactoring. Let's take this opportunity to learn how to use the case
statement.
After the change run the tests again to make sure you didn't break anything.
The code is working as expected, but I'm starting to feel like #hello
is accumulating too much logic in it.
I want to create a new method just to handle the multilingual greeting. As such method is not intended to be called from the "outside world", but only from the #hello
method, we can define it as a private method.
A private method can only be used internally by the object. Any attempt to call it from outside will result in an error. For example, if you try to execute greeter.greeting
, you'll get an error.
Alright, with the #greeting
method our greeter.rb
becomes like this:
Run the tests and it should pass.
One question that might arise here is: "shouldn't we create a test for the greeting method?". And my answer for this case is: no. We want to validate the desired behavior, which is to greet people in a proper language. Our test suite is validating this behavior through the #hello
method.
OK, moving on...
Passing tests triggers a decision for us: refactor or stop coding this feature?
I still want a refactoring to make the #greeting
method more idiomatic.
First, Rubyists usually put the private
keyword in a single line. When we do this, all methods below that line is considered private.
Run the test and it should pass. Next refactoring...
Remember when I said that the last evaluated expression of a ruby method is always returned? The whole case
block is an expression and we can make it the last evaluated one:
Run the tests again and it should pass.
Let's commit what we've done so far:
As an exercise add greetings for other languages.
Remember the cycle:
Write a test
Run the test, see it failing and check the error message
Write enough code to make the test pass
Refactor
This is probably the fanciest Hello, World
you have ever written, isn't it?
We learn a bunch of things here.
how to create a class
how to create methods (and private methods)
how to create a constructor method (initialize
)
instance variables are prefixed with the @
symbol
parentheses in method calls are optional
the last evaluated expression of a method is always returned
string interpolation (e.g.: "Hello, #{name}"
)
if-else
and if-elsif-else
statements
case
statements
how to write tests with Minitest (it's just Ruby)
The TDD process and why the steps are important
Write a failing test and see it fail
so we know we have written a relevant test for our requirements
and see that it produces an easy to understand description of the failure
Writing the smallest amount of code to make it pass
so we know we have working software
Then refactor, backed with the safety of our tests
to ensure we have well-crafted code that is easy to work with
We've gone from greeter.hello
to greeter.hello("name")
and then to spanish_hello("name")
in small and easy to understand steps.
Of course this is trivial compared to "real-world" software, but the principles still stand.
TDD is a skill that needs practice to develop, and by breaking problems down into smaller components that you can test, you will have a much easier time writing and reading software.
Our Greeter
currently defines only a method (the behavior), but in the we mentioned that OOP is about grouping together data and behavior. Now it's a good time to make use of an instance variable to hold the greeter's language.