Rails Tutorial: How to Build a Subscription Form that Integrates with Mailchimp Part 1-(models and tests)
Photo by Phil Goodwin on Unsplash
I’ve recently been doing some pair programming, and one of the recent projects was building a subscriber form that would save the email address to Mailchimp.
On the surface, Rails makes this easy. We could use the rails scaffold generators to get most of the work done and then use a gem to integrate with Mailchimp.
However, even though it was simple on the surface, I decided to write in detail about the different edge cases that can come up.
Most tutorials on Rails assume the best-case scenario. However, the world is cruel and bad stuff happens. Here, I will go through how to build a subscriber form that integrates with Rails step-by-step and address issues as they come up. We’re also going to take the time to explore what’s happening under the hood.
The criteria are as follows:
- Capture a valid email address and name
- Ensure that users can’t see who’s also subscribed
- Sync these emails with Mailchimp
1. Create a new Rails app
First things, let generate a new rails app followed by a subscriber scaffold.
In the terminal, type the following:
rails new subscriber_app
Next, we change into that directory.
cd subscriber_app
Now we run in the local Rails server.
rails s
You can also run this in a separate terminal, but it’s not necessary for this tutorial. Webpacker is useful when working with Javascript packages and Tailwindcss.
bin/webpack-dev-server
You should now be able to navigate to localhost:3000
Under the Hood
Running the rails new
command generates a new rails app with the default rails directory. Rails will help a lot in getting us started. The directory structure and default code come from the philosophy convention over configuration. This essentially means that the authors of Rails have decided to guide new projects to have the same directory structure and boilerplate code as any other Rails app.
Rails will use an MVC structure which is probably the most famous structure for web applications.
2. Generate a subscriber Scaffold
To create a subscriber form, we need to think about the kind of data we need to capture.
Luckily, there are millions of examples of subscriber forms around the internet. Mailchimp only needs an email address. The name field is optional.
So we know that we definitely need an email address and possibly a name. Now we think about what that data means when we try and translate it to rails.
rails generate scaffold subscriber name:string email:string
This will generate a lot of files:
subscriber_app git:(master) ✗ rails generate scaffold subscriber name:string email:string
Running via Spring preloader in process 53538
invoke active_record
create db/migrate/20210317173258_create_subscribers.rb
create app/models/subscriber.rb
invoke test_unit
create test/models/subscriber_test.rb
create test/fixtures/subscribers.yml
invoke resource_route
route resources :subscribers
invoke scaffold_controller
create app/controllers/subscribers_controller.rb
invoke erb
create app/views/subscribers
create app/views/subscribers/index.html.erb
create app/views/subscribers/edit.html.erb
create app/views/subscribers/show.html.erb
create app/views/subscribers/new.html.erb
create app/views/subscribers/_form.html.erb
invoke resource_route
invoke test_unit
create test/controllers/subscribers_controller_test.rb
create test/system/subscribers_test.rb
invoke helper
create app/helpers/subscribers_helper.rb
invoke test_unit
invoke jbuilder
create app/views/subscribers/index.json.jbuilder
create app/views/subscribers/show.json.jbuilder
create app/views/subscribers/_subscriber.json.jbuilder
invoke assets
invoke scss
create app/assets/stylesheets/subscribers.scss
invoke scss
create app/assets/stylesheets/scaffolds.scss
It will also create a migration file in your db/migrate
with the following:
class CreateSubscribers < ActiveRecord::Migration[6.1]
def change
create_table :subscribers do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
If you navigate to localhost:3000, the browser will present you with an error screen with a prompt telling you to run migrations.
You can either press the button or type the following into your terminal:
rails db:migrate
Under the hood
We just saw that Rails generates a lot of files with the Rails scaffold command. It’s pretty cool. Can you imagine having to remember to create all those files yourself?
All the code conforms to the MVC architecture we alluded to earlier. Your config/routes.rb should look like the following.
# config/routes.rb
Rails.application.routes.draw do
resources :subscribers
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end
The resources
method does something powerful. When a user navigates to /subscribers on your app, the router points towards the controller action which in turn renders the view.
- User navigates to subscribers/new
- The resources method now knows what controller method to run. In this case, it’s going to be the
new
method. - In the
subscribers_controller.rb
file, we can see thenew
method. - The new method knows to look in the
app/views/subscribers
folder to render the correct HTML file. In this case,new.html.erb
- This is the HTML the user sees on their browser.
This can all seem vague initially, but the more you do it, the less surprising it gets.
Finally, we have the migration file.
This is the file that describes what we are going to do with our database. In this case, we are creating a table called subscribers with a name column and an email column.
What can go wrong
The scaffold is Rails greatest strength but also the newcomers greatest weakness. When you’re first getting started, it can be hard to know why Rails does all this.
The only way to understand is to do it more.
Looking at our app, we can see that Rails scaffold has helped us a lot, but according to our initial criteria, it’s a resounding failure.
We can see every subscriber; we can enter weird email addresses and submit blank values.
As well as that, the app is not too stylish.
3. Making the app more robust - Validations with Tests
At the moment, a user can enter any email address they want. However, we can’t have bad data getting into our app.
The first thing we need to do is prevent bad data from getting in.
Open up app/models/subscriber.rb
This file is where we put all our business logic related to the subscriber class. This is usually everything we want the subscriber object to do and know.
If you remember, we have the following criteria:
Capture a valid email address and name
How do we make sure we only save valid email addresses and names?
Rails has got your back with something called ActiveRecord Validations. These are nifty methods you can use in your models to ensure only good data goes into your database.
Before we add validations to our subscriber model, let’s write some tests first.
In test/models/subscriber_test.rb
, write the following:
require "test_helper"
class SubscriberTest < ActiveSupport::TestCase
test 'invalid if email is nil' do
subscriber = Subscriber.new(name: 'John Doe', email: nil)
assert subscriber.invalid?
end
end
Now we can run this test with the following command in your terminal.
rails test test/models/subscriber_test.rb
Before you run this command, stop and ask yourself the question.
What will the command tell us?
Take 10 seconds and answer.
If you said that it tells us our test failed, you would be correct.
You should see an output like this.
Running via Spring preloader in process 9287
Run options: --seed 17734
# Running:
F
Failure:
SubscriberTest#test_invalid_if_email_is_nil [/Users/williamkennedy/projects/teaching_rails/subscriber_app/test/models/subscriber_test.rb:7]:
Expected false to be truthy.
rails test test/models/subscriber_test.rb:5
Finished in 0.204865s, 4.8813 runs/s, 4.8813 assertions/s.
1 runs, 1 assertions, 1 failures, 0 errors, 0 skips
Pay attention to the last line.
It says we have run 1 test, made one assertion and had one failure.
Believe it or not, this is a good thing. You have taken a step towards making your app more maintainable.
Writing the test first and then writing the code that makes the test pass is known as Test-Driven Development. It is a popular form of development that everyone says they do, needs to do, wants to do and sometimes rarely does.
I’ll cover the nuances of testing later but suffice to say that when used correctly, they can be a real lifesaver, and when misused, it can lead to a whole world of pain.
So how do we make this test pass? In our subscriber model, add the following line:
# app/models/subscriber.RB
validates :email, presence: true
Now that you’ve done that, rerun the tests.
rails test test/models/subscriber_test.rb
Now your output will look like the following.
rails test test/models/subscriber_test.rb:5
# Running:
.
Finished in 0.204100s, 4.8996 runs/s, 4.8996 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
Woohoo, our test passed. If we go to our browser, we can now see that we cannot submit an email without filling the email field.
However, we can still submit an invalid email. Let’s add three more tests in “test/models/subscriber_test.RB
`` to ensure we are saving correct emails.
test 'invalid if email has no @ symbol' do
subscriber = Subscriber.new(name: 'John doe', email: 'test.coa')
assert subscriber.invalid?
end
test 'invalid if email has no space in it' do
subscriber = Subscriber.new(name: 'John doe', email: 'j @test.coa')
assert subscriber.invalid?
end
test 'invalid if email has space at end' do
subscriber = Subscriber.new(name: 'John doe', email: 'j@test.com ')
assert subscriber.invalid?
end
Rerunning the tests will show that we have three failures. Now let’s fix that.
In your app/models/subscriber.rb
, add the following:
validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i }
This will make out tests pass, but it’s not ideal. An email has many nuances. I would encourage you to add more tests and improve it or use a Gem to validate the email yourself. For now, this will work.
Finally, we need to make sure our subscriber has a name. So let’s write a test for that as well.
In tests/models/subscriber_test.rb
test 'invalid if name is nil' do
subscriber = Subscriber.new(name: nil, email: 'test@test.com')
assert subscriber.invalid?
end
Now run the test to make sure it fails/
Finally, let’s make this test pass:
In your app/models/subscriber.rb
, add name to our first validation:
validates :email, :name, presence: true
Congrats. You have made your data safe. Now before we move on to part 2, let’s review.
Under the hood
In this section, we practised Test-Driven Development. TDD means we write the test first and then write the code to pass the test.
In practice, writing tests is a good thing. However, you will find many codebases in the real world that don’t have tests for every piece of code written. The bigger the codebase, the more you need tests.
Sometimes that can be OK. Other times, not so much. As much as possible, try to have some tests.
Possible Edge cases
If you look long enough and try long enough, we can find lots of edge cases. For example, we are just checking if it’s a valid email format and not checking if it’s an actual email.
We are only checking for the presence of the name. Perhaps, we need to capture the first name and last name.
There may be more. Can you think of any?