Rails Tutorial: How to Build a Subscription Form that Integrates with Mailchimp Part 1-(models and tests)

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.

  1. User navigates to subscribers/new
  2. The resources method now knows what controller method to run. In this case, it’s going to be the new method.
  3. In the subscribers_controller.rb file, we can see the new method.
  4. The new method knows to look in the app/views/subscribers folder to render the correct HTML file. In this case, new.html.erb
  5. 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: '[email protected] ')
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: '[email protected]')
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?

22