Rails Model and Service Object Testing Using RSpec and FactoryBot. Case : E-money Transfer | by Agung Setiawan | Medium

Ask questions Research chat →

https://medium.com/@agungsetiawan/rails-model-and-service-object-testing-using-rspec-and-factorybot-case-e-money-transfer-a60865b48baf · scraped

rails testing

Attachments

Scraped Content

— 1658 words · 2026-02-14 17:39:59 UTC ·

Excerpt

![](https://prod-files-secure.s3.us-west-2.amazonaws.com/871f1661-80b8-4d0c-ac3b-2adfc6ff4c66/02816be9-0b8e-4c68-8e26-ad710ddacd64/0YPMo1N22fllvehNr?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466THIYANRN%2F20260214%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20260214T173958Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEEEaCXVzLXdlc3QtMiJHMEUCIEUp6pTjG1c5SX9V03TUu8TZorxAcDJjFmUyzdqE27M3AiEAsEymD269lvODlSf7S37TtSHk%2B1ZA981HVd4D0KxuDj4q%2FwMIChAAGgw2Mzc0MjMxODM4MDUiDHp4JSO7Pt9%2Br4FCMCrcAw9x3X0mG9saveHSwn8IDQDLxgn1FrTw0R8lrHyKCnd6Ftld6YaOGsnS63foO5v1PwZA%2FnuD7%2BH91kHX09tipBnkywdOXKJzhqXHu4M5iY%2FXS97uqwBYJjKFwXVYIDQFDZD58psH957bxPBM8bRG6Zc3MXYFQoyTIaN%2BdvB0%2BWcCCfXUh%2BmH9rI5lJR5ZKZpb3gPqaD12MxkraQA086O85BO2gE9rFrrt0nkFCtt7jwOW8vmtuaN3FvVEHr6547mL1KeWw622VCzGYCKRhDFB1ILjJycELJdjqFtX4wMVQCx4CEoQ2KgsJCsQH72p3VaGE0mHjs%2Fe6xqUbl7RERz%2B9NOy0izhh2%2F00qH2uQZDzAWWGnS0bkJSoQWE8eiHiuJk0acI%2FRrJDONcRMDJSn63KVyfe51zGbk3lf8mnQaY
![](https://prod-files-secure.s3.us-west-2.amazonaws.com/871f1661-80b8-4d0c-ac3b-2adfc6ff4c66/02816be9-0b8e-4c68-8e26-ad710ddacd64/0YPMo1N22fllvehNr?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466THIYANRN%2F20260214%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20260214T173958Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEEEaCXVzLXdlc3QtMiJHMEUCIEUp6pTjG1c5SX9V03TUu8TZorxAcDJjFmUyzdqE27M3AiEAsEymD269lvODlSf7S37TtSHk%2B1ZA981HVd4D0KxuDj4q%2FwMIChAAGgw2Mzc0MjMxODM4MDUiDHp4JSO7Pt9%2Br4FCMCrcAw9x3X0mG9saveHSwn8IDQDLxgn1FrTw0R8lrHyKCnd6Ftld6YaOGsnS63foO5v1PwZA%2FnuD7%2BH91kHX09tipBnkywdOXKJzhqXHu4M5iY%2FXS97uqwBYJjKFwXVYIDQFDZD58psH957bxPBM8bRG6Zc3MXYFQoyTIaN%2BdvB0%2BWcCCfXUh%2BmH9rI5lJR5ZKZpb3gPqaD12MxkraQA086O85BO2gE9rFrrt0nkFCtt7jwOW8vmtuaN3FvVEHr6547mL1KeWw622VCzGYCKRhDFB1ILjJycELJdjqFtX4wMVQCx4CEoQ2KgsJCsQH72p3VaGE0mHjs%2Fe6xqUbl7RERz%2B9NOy0izhh2%2F00qH2uQZDzAWWGnS0bkJSoQWE8eiHiuJk0acI%2FRrJDONcRMDJSn63KVyfe51zGbk3lf8mnQaYr2xJ4r7%2BmiqEgAy0q2UtnaL8xNYHm%2FsBfAWQv89g5mfNPnbRms%2BAUrJbsv04W%2FQsQuynBuh7c5b4p9KuS%2F7UsDH%2Bhnmfw%2B35N7kA22ZQHbf%2FNO%2FNx3Q0Q%2Bw%2BlDmHHFbP%2ByMqznEdJZ%2FgTbl39qaFqsT4teK4V6VXZr%2B%2FIDnMoM1Mj1SfaVYyq4Ng%2Bl4n07AgQuzlpmD7KzZfy7jvS0PMKvRwswGOqUBhTh%2FKGULfSwxJZb53sBTRCGcBfJu83MdQBBe08bNXWz8cQX8smK70WeiTY3vPtnUmJsD2tCzRWD2%2BuaiUAm15AIj5g2RNk9jTd%2FYtHCtnu0oczs4Gazqq0CR%2FEwDHf7YFqGI9oFXEKX0TsxfNgMVeW14P8a7blrau3Dle9OnVv4EQFX9JrJ6wPdObLBnCyNERCuyRrBB24A8gT0wSA2TZl4QLM84&X-Amz-Signature=23f991d0cc57abe201e8c338b57867afd1079325fbb3756a83d9f73d1a6f3c55&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject) Photo by Patrick Ward on Unsplash I don’t thing it exists, such Ruby/Ruby on Rails book or online course or any other materials that teach us how to write automated test on earlier chapters when the target audience are people relatively new to web development. It’s kind of common sense that we will (if given any time, *ahemm, common excuse*) write tests later after the code itself is done, so we can agree that for new comers it is definitely more important to teach them how to write code first. At some point in our career we will be expected to be able to write tests either using community most popular testing framework RSpec or using Rails’ default, Minitest. We will proceed with RSpec in this tutorial. # Why Bother Writing Test? There are two main benefits of writing automated tests in my opinion. The first one is we can tests our code easily, relatively fast and can do it repeatedly as many as we want. The second is it gives us confidence in doing code refactoring. Let’s first talk about point one. Don’t get me wrong with the word “easily”, I didn’t mean that it is easy to write tests. What I really mean is once we have tests, we can run it easily. It’s also worth mentioning that once we have tests we can run it repeatedly in relatively short amount of time compared to manual testing, there will be huge gap on that. Now, how will you get fast feedback after doing some code refactoring? Manual testing? Too slow. Deploy to production and wait until users complain? Not a good one either. The answer is automated tests. When we have it, we can run tests to make sure whether our code changes break the tests or not. If it isn’t, that’s okay. If it does break then we need to check our changes and fix it and make sure everything is okay when we run the tests again. # What Will You Learn We’re focusing on testing model and service object using RSpec with E-Money transfer between accounts as an example. This isn’t going to be a complete application, it doesn’t even have any controllers and views as we solely focus on testing model and service object. Don’t worry, you will get new knowledge as a reward :) We’re also going to use FactoryBot to create necessary ActiveRecord object in testing. I can tell you that RSpec + FactoryBot for testing is awesome. Let’s get started. Both of the tools are not Rails built in, so we need to add them in Gemfile on both :development and :test group. Don’t forget to actually download and install them by running bundle install or simply just bundle from command line within project directory. A few more steps needed. Generate boilerplate configuration files by running rails generate rspec:install resulting in creating these files ```plain text create .rspec create spec create spec/spec_helper.rb create spec/rails_helper.rb ``` Lastly open spec/rails_helper.rb and add following configuration. # Model Testing ## The models We use 3 models to create the application we are going to test. There are User class, Account class, and lastly Transaction class. Please check code below for better understanding of their relationship with each other and what instance methods exist on a class. We use Account to model a bank/e-money account. We can use balance as the main attribute here to hold how much money an account has and also there should be user_id that acts as foreign key to connect it with users table. Each account can have many transactions. This is make sense since in the real world we can buy or transfer our money, hence make a transaction, as many as possible as long as we have the balance. The transactions we made is represented using Transaction model class. For the sake of simplicity, we design the table and model to only support transfer between account as type of transaction. See instance methods on User and Transaction? We will write model tests for those methods. ## The migrations I don’t want you to get lost following this tutorial. Here’s the migrations for reference. ## The tests Since we have integrated RSpec and FactoryBot into our project, it will generate test related files automatically when we use Rails generator to create models. ![](https://prod-files-secure.s3.us-west-2.amazonaws.com/871f1661-80b8-4d0c-ac3b-2adfc6ff4c66/c351871b-7d75-4f48-afe4-cf978f90fd59/1M68csMPgLUMqG5yW_Vtceg.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466THIYANRN%2F20260214%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20260214T173958Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEEEaCXVzLXdlc3QtMiJHMEUCIEUp6pTjG1c5SX9V03TUu8TZorxAcDJjFmUyzdqE27M3AiEAsEymD269lvODlSf7S37TtSHk%2B1ZA981HVd4D0KxuDj4q%2FwMIChAAGgw2Mzc0MjMxODM4MDUiDHp4JSO7Pt9%2Br4FCMCrcAw9x3X0mG9saveHSwn8IDQDLxgn1FrTw0R8lrHyKCnd6Ftld6YaOGsnS63foO5v1PwZA%2FnuD7%2BH91kHX09tipBnkywdOXKJzhqXHu4M5iY%2FXS97uqwBYJjKFwXVYIDQFDZD58psH957bxPBM8bRG6Zc3MXYFQoyTIaN%2BdvB0%2BWcCCfXUh%2BmH9rI5lJR5ZKZpb3gPqaD12MxkraQA086O85BO2gE9rFrrt0nkFCtt7jwOW8vmtuaN3FvVEHr6547mL1KeWw622VCzGYCKRhDFB1ILjJycELJdjqFtX4wMVQCx4CEoQ2KgsJCsQH72p3VaGE0mHjs%2Fe6xqUbl7RERz%2B9NOy0izhh2%2F00qH2uQZDzAWWGnS0bkJSoQWE8eiHiuJk0acI%2FRrJDONcRMDJSn63KVyfe51zGbk3lf8mnQaYr2xJ4r7%2BmiqEgAy0q2UtnaL8xNYHm%2FsBfAWQv89g5mfNPnbRms%2BAUrJbsv04W%2FQsQuynBuh7c5b4p9KuS%2F7UsDH%2Bhnmfw%2B35N7kA22ZQHbf%2FNO%2FNx3Q0Q%2Bw%2BlDmHHFbP%2ByMqznEdJZ%2FgTbl39qaFqsT4teK4V6VXZr%2B%2FIDnMoM1Mj1SfaVYyq4Ng%2Bl4n07AgQuzlpmD7KzZfy7jvS0PMKvRwswGOqUBhTh%2FKGULfSwxJZb53sBTRCGcBfJu83MdQBBe08bNXWz8cQX8smK70WeiTY3vPtnUmJsD2tCzRWD2%2BuaiUAm15AIj5g2RNk9jTd%2FYtHCtnu0oczs4Gazqq0CR%2FEwDHf7YFqGI9oFXEKX0TsxfNgMVeW14P8a7blrau3Dle9OnVv4EQFX9JrJ6wPdObLBnCyNERCuyRrBB24A8gT0wSA2TZl4QLM84&X-Amz-Signature=6aafc78f41b7d9a155ddfd763674c035a283db76aa1b6feb9618231d4f75af5c&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject) From the output as shown above we know that file responsible for User model testing has been created and named user_spec.rb . This is RSpec convention, every test file is located in the same folder structure with the code being tested within spec folder and put _spec suffix for the file name. So if we have app/models/user.rb then the test file is spec/models/user_spec.rb , app/services/transfer_money_service.rb will have spec/services/transfer_money_service_spec.tb Rails also generate spec/factories/users.rb . We’ll use it to create ActiveRecord object when doing testing, detail later. Now if you open user_spec.rb it will have default content like this Let’s do the work! What we’re going to do is test priority_user? method on User class. For short it can be written like this : User#priority_user? . # indicate that priority_user? is an instance method. Knowing this little stuff is important, please do check the testing code below. We want to test priority_user? method so what we first do is creating describe block for that purpose. Inside this block, we write the tests. User#priority_user? can return either true or false depending on the account balance a user has, whether more or less than $100,000. We clearly want to test both of possibility. Thus we create two it blocks that are used for the actual testing. We are now begin to leverage FactoryBot in our test. You see create method used inside the tests? In our case, we need to create user and account record in database and also have the instance of those objects. And so we’re using FactoryBot to help us. Once we have done the preparation, we test the priority_user? method using expect to and eq. You can read it like regular English : “Expect the return of user.prioriy_user? to equal true/false”. Now we can run the tests and see if all is good by executing this command from project directory. ```plain text rspec spec/models/user_spec.rb..... Finished in 0.07544 seconds (files took 1.68 seconds to load) 2 examples, 0 failures ``` Yay, 0 failures indicating that the tests pass and all went good. ## Cleaner test Did you notice that both tests need to create user record first using create(:user) ? We can simplify it using let block. let needs symbol as parameter, this will be used for a reference. In our case, we use let to create user record. The block that let has will not be invoked unless we explicitly call it. This means after line 5 we still don’t have user record in our database. Once we call it using user reference (the same name with the :user symbol) on line 8 (and later line 14), a user record has been created. Run rspec spec/models/user_spec.rb again and it should still pass. We’re not finish yet to make the tests better. We can use context block to give clear context for the tests code. As shown below Don’t forget to run rspec spec/models/user_spec.rb For exercise, how about you write tests for Transaction#completed? , Transaction#pending? and Transaction#failed? # Service Object Testing Service object testing is no different than model testing, in fact we will still be using model test to test our service object. ## The service object class There’s plenty of blog posts talking about service object already. In essence, it encapsulates one business logic into a class with single public method, usually called call. The name of the class indicates what’s the class is doing. Since our users are going to be able to transfer money to their friends, we named our class as TransferMoneyService. The file location is at app/services/transfer_money_service.rb Does it look monstrous? Fear not, read carefully and if needed read over and over again to understand it, take your time. Basically what it’s doing is subtract balance from sending user, add balance to receiving user and create transactions record for both sender and receiver. ## The test This time, Rails won’t generate test file for us. We should manually create spec/services/transfer_money_service_spec.rb. For starter, here’s how the test looks like. Did you see something new? In addition to let, there’s let! . The difference between the two is let won’t execute the code inside block until it’s getting called, whereas let! will be immediately invoked. Let’s write the actual test. We want to make sure that after a user transfer his/her money to someone else, his/her balance will be subtracted and someone else balance will be added. With initial balance 1000, sending 500 means now the final balance is 500. As for receiver, with initial balance 0, receiving 500 means the balance is now 500. Run rspec spec/service/transfer_money_service_spec.rb to see the result. ```plain text rspec spec/service/transfer_money_service_spec.rb..... Finished in 0.11992 seconds (files took 2.53 seconds to load) 1 example, 0 failures ``` So far everything’s good!!! Let’s continue the test. We need to check the existence of Transaction record and make sure its attributes value is right. Here’s the final code. Nothing new here in terms of RSpec’s special method to write test. All of them has been introduced earlier in this blog post. Now run rspec spec/service/transfer_money_service_spec.rb again this time. Everything should be good. ## Confident in refactoring The test will give us confidence in doing code refactoring. If we decide that we want to refactor TransferMoneyService for whatever reason, for instance to make it more readable (because I think it’s readable already 😄), we can have fast feedback loop by running the test. # Github Repository If you would like to check the complete code we talk about in this blogpost, please visit https://github.com/agungsetiawan/emoney-transfer-testing I’m more than happy to answer your question and have discussion around this topic with you.

Visibility

Visible to everyone

Reading Status

Related Bookmarks

My Note


Saved!

Annotations

Export as Markdown
+ Annotate selection

Add Annotation