Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Counterintuitive Rails

Counterintuitive Rails

Ivan Nemytchenko

March 16, 2018
Tweet

More Decks by Ivan Nemytchenko

Other Decks in Programming

Transcript

  1. THE AUTHOR IVAN NEMYTCHENKO Started using Rails in 2006 Did

    lots of different stuff (freelancing, agencies, conferences, lean poker, teaching) Former GitLab developer advocate (stickers!)
  2. THE AUTHOR IVAN NEMYTCHENKO Was giving a talk here 3

    years ago Created RailsHurts.com Started writing a book
  3. AN IDEAL SYSTEM 1) Must oppose to a bad code

    2) Doesn’t fight against the framework 3) There should be one way of doing a specific thing
  4. HIERARCHY BASED ON ENTITIES’ DEPENDENCIES class Article < ApplicationRecord has_many

    :comments, dependent: :destroy has_many :links, dependent: :destroy, inverse_of: :article has_many :likes, class_name: "Article::Comment::Like" end class Article::Link < ApplicationRecord belongs_to :article validates :url, presence: true end class Article::Comment < ApplicationRecord belongs_to :article has_many :likes end class Article::Category < ApplicationRecord has_many :articles end class Article::Comment::Like < ApplicationRecord belongs_to :comment belongs_to :article end BETTER WAY OF DOING IT:
  5. BETTER WAY OF DOING IT: HIERARCHY BASED ON ENTITIES’ DEPENDENCIES

    ⊕ Makes it easier to understand what is your app about ⊕ Zero long term cost ⊕ Rails fully supports this ⊖ sometimes: class_name: “Article::Comment::Like" ± have to think about "dependencies"
  6. SMALL SCALE TYPICAL WAY OF DOING IT: class InvitesController <

    ApplicationController requires_login only: [ :destroy, :create, :create_invite_link, :rescind_all_invites, :resend_invite, :resend_all_invites, :upload_csv ] skip_before_action :check_xhr, except: [:perform_accept_invitation] skip_before_action :preload_json, except: [:show] skip_before_action :redirect_to_login_if_required before_action :ensure_new_registrations_allowed, only: [:show, :perform_accept_invitation before_action :ensure_not_logged_in, only: [:show, :perform_accept_invitation] #...
  7. LARGER SCALE TYPICAL WAY OF DOING IT: class PostsController <

    ApplicationController requires_login except: [ :show, :replies, :by_number, :short_link, :reply_history, :replyIids, :revisions, :latest_revision, :expand_embed, :markdown_id, :markdown_num, :cooked, :latest, :user_posts_feed ] skip_before_action :preload_json, :check_xhr, only: [ :markdown_id, :markdown_num, :short_link, :latest, :user_posts_feed ] #...
  8. LARGER SCALE TYPICAL WAY OF DOING IT: class UsersController <

    ApplicationController skip_before_action :authorize_mini_profiler, only: [:avatar] requires_login only: [ :username, :update, :user_preferences_redirect, :upload_user_image, :pick_avatar, :destroy_user_image, :destroy, :check_emails, :topic_tracking_state, :preferences, :create_second_factor, :update_second_factor ] skip_before_action :check_xhr, only: [ :show, :badges, :password_reset, :update, :account_created, :activate_account, :perform_account_activation, :user_preferences_redirect, :avatar, :my_redirect, :toggle_anon, :admin_login, :confirm_admin, :email_login ] before_action :respond_to_suspicious_request, only: [:create] skip_before_action :verify_authenticity_token, only: [:create] skip_before_action :redirect_to_login_if_required, only: [:check_username, :create, :get_honeypot_value, :account_created, :activate_account, :perform_account_activation, :send_activation_email, :update_activation_email, :password_reset, :confirm_email_token, :email_login, :admin_login, :confirm_admin]
  9. FLAT CONTROLLERS STRUCTURE - 1) REST misunderstanding - 2) respond_to

    - 3) lack of DDD thinking WHY DO WE DO THAT?
  10. REST IS ALL ABOUT REPRESENTATIONS! - You never get the

    resource itself - There could be many representations, and it is fine! - They can be very different!
  11. SMALL SCALE TYPICAL WAY OF DOING IT: respond_to do |format|

    format.html { render locals: {query: params[:q]} } format.json { @devices = @devices.first(5) @employees = @employees.first(3) } end
  12. LARGER SCALE (REDMINE) TYPICAL WAY OF DOING IT: respond_to do

    |format| format.html { @issue_count = @query.issue_count @issue_pages = Paginator.new @issue_count, per_page_option, params['page'] @issues = @query.issues(:offset => @issue_pages.offset, :limit => @issue_pages.per_page) render :layout => !request.xhr? } format.api { @offset, @limit = api_offset_and_limit @query.column_names = %w(author) @issue_count = @query.issue_count @issues = @query.issues(:offset => @offset, :limit => @limit) Issue.load_visible_relations(@issues) if include_in_api_response?('relations') } format.atom { @issues = @query.issues(:limit => Setting.feeds_limit.to_i) render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") } format.csv { @issues = @query.issues(:limit => Setting.issues_export_limit.to_i) send_data(query_to_csv(@issues, @query, params[:csv]), :type => 'text/csv; header=present', :filen => 'issues.csv') } format.pdf { @issues = @query.issues(:limit => Setting.issues_export_limit.to_i) send_file_headers! :type => 'application/pdf', :filename => 'issues.pdf' } end
  13. TEXT HTML, JSON, PDF, RSS - they all used in

    different ways - different subsets of data - different ways to generate results So it makes sense to create separate REST representations
  14. BETTER WAY OF DOING IT: TOP-LEVEL NAMESPACES class Web::ArticlesController <

    Web::ApplicationController #... end class Api::ArticlesController < Api::ApplicationController #... end
  15. DOMAIN DRIVEN DESIGN BOUNDED CONTEXT EXAMPLE Creation/editing: Article - title

    - body - author Moderation: Article - title - body - author + category + publication_date
  16. BETTER WAY OF DOING IT: BOUNDED CONTEXT EXAMPLE - No

    such thing as universal article here - Representation depends on context both in terms of behaviour and data sets
  17. BETTER WAY OF DOING IT: HIERARCHY BASED ON “BOUNDED CONTEXTS”

    scope module: :web do namespace :moderation do resources :articles, only: [:index, :edit, :update, :show] do member do patch :publish end end end resources :articles do member do patch :moderate end scope module: :articles do resources :comments do scope module: :comments do resources :likes, only: [:create] end end end end root 'welcome#index' end
  18. BETTER WAY OF DOING IT: HIERARCHY BASED ON ENTITIES’ DEPENDENCIES

    complex universal controllers vs many simple controllers for specific representations
  19. BETTER WAY OF DOING IT: HIERARCHY BASED ON ENTITIES’ DEPENDENCIES

    ⊕ Makes it easier to understand what is your app about ⊕ Zero long term cost ⊕ Rails fully supports this ⊕ URLs structure matches folders structure ⊕ Reduces number of if-statements in controllers & views ⊖ Longer controller names
  20. FLAT CONTROLLERS STRUCTURE - 1) No respond_to - 2) Separate

    namespaces per way of usage - 3) More namespaces per “bounded context” inside HOW TO FIX:
  21. VIOLATING MVC - using Rails features, that violate it -

    ignoring differences between application layers - using gems that violate MVC - not distinguishing Application logic from Business logic HOW DO WE VIOLATE MVC?
  22. TYPICAL WAY OF DOING IT: APPLICATION LOGIC IN MODELS class

    Person < ActiveRecord::Base validates_confirmation_of :user_name, :password validates_confirmation_of :email_address validates_acceptance_of :terms_of_service validates_acceptance_of :eula end
  23. - Rails gives us this stuff - It is definitely

    the application logic - Still it is in our models WHAT IS WRONG ABOUT IT? APPLICATION LOGIC IN MODELS
  24. WHAT ARE MODELS ABOUT? PERSISTENCE? - We can have models

    without persistence - Persistence is a technical detail
  25. WHAT ARE MODELS ABOUT? DOMAIN MODEL - It is a

    system that imitates structure or functioning of explored domain
  26. WHAT ARE MODELS ABOUT? - doesn’t depend on programming language

    - doesn’t depend on type of app - doesn’t depend on framework DOMAIN MODEL
  27. TYPICAL WAY OF DOING IT: LET’S JUST PUT EVERYTHING INTO

    MODELS FAT MODELS, SKINNY CONTROLLERS!
  28. HOW COMPLEXITY CREEPS IN? IT ALL STARTS PRETTY INNOCENT def

    create @employee = Employee.new(employee_params) if @employee.save flash.notice = "Employee updated!" redirect_to employee_path(@employee) else flash.alert = "Couldn't update employee!" render :edit end end
  29. HOW COMPLEXITY CREEPS IN? THEN IT BECOMES A BIT MORE

    MESSY def create @employee = Employee.new(employee_params) if @employee.save if params[:set_as_admin] == "1" User.create( email: employee.email, name: employee.name, employee: employee ) end if params[:add_another] == "1" redirect_to new_employee_path else redirect_to employees_path end else render :new flash.alert = "Couldn't add employee" end end
  30. DEFINITION SERVICE Service encapsulates single process of our business. They

    take all collaborators (database, logging, external adapters like Facebook, user parameters) and performs a given process. Services belongs to our domain - they shouldn’t know they’re within Rails or webapp!
  31. FIGHTING COMPLEXITY BACK PUT STUFF INTO PROPER PLACES class CreateEmployee

    def self.process(params, set_as_admin) employee = Employee.new(employee_params) ActiveRecord::Base.transaction do employee.save if set_as_admin User.create( email: employee.email, name: employee.name, employee: employee ) end end employee end end def create @employee = CreateEmployee.process( employee_params, params[:set_as_admin] ) if @employee.valid? redirect_to employees_path else render :new flash.alert = "Couldn't add employee" end end
  32. DEFINITION SERVICE In DDD, Evans defines a service as an

    action, not a thing. Instead of forcing the operation into an existing object, we should encapsulate it in a separate, stateless service.
  33. TYPICAL WAY OF DOING IT: SERVICE class TripReservationService attr_reader :payment_adapter,

    :logger def initialize(payment_adapter, logger) @payment_adapter = payment_adapter @logger = logger end def process(user, trip, agency, reservation) raise AgencyRejectionError.new unless user.can_book_from?(agency) raise NoTicketError.new unless trip.has_free_tickets? begin receipt = payment_adapter.pay(trip.price) reservation.receipt_id = receipt.uuid unless reservation.save logger.info "Failed to save reservation: #{reservation.errors.inspect}" raise ReservationError.new end rescue PaymentError logger.info "User #{user.name} failed to pay for a trip #{trip.name}: #{$!.message}" raise TripPaymentError.new $!.message end end end
  34. TYPICAL WAY OF DOING IT: SERVICE class TripReservationService attr_reader :payment_adapter,

    :logger def initialize(payment_adapter, logger) @payment_adapter = payment_adapter @logger = logger end def process(user, trip, agency, reservation) raise AgencyRejectionError.new unless user.can_book_from?(agency) raise NoTicketError.new unless trip.has_free_tickets? begin receipt = payment_adapter.pay(trip.price) reservation.receipt_id = receipt.uuid unless reservation.save logger.info "Failed to save reservation: #{reservation.errors.inspect}" raise ReservationError.new end rescue PaymentError logger.info "User #{user.name} failed to pay for a trip #{trip.name}: #{$!.message}" raise TripPaymentError.new $!.message end end end
  35. BETTER WAY OF DOING IT: USING DEPENDENCY INJECTION CONTAINER Rails.application.configure

    do config.container = Dry::Container.new if Rails.env.test? config.container.register(:stripe_klass, -> { StubPaymentProcessor }) # ... else config.container.register(:stripe_klass, -> { StripePaymentProcessor }) # … end end stripe_klass = Rails.application.config.container.resolve(:stripe_klass)
  36. TYPICAL WAY OF DOING IT: WITH INTERACTOR GEM class CreateEmployee

    include Interactor def call employee = Employee.new(organisation: context.organisation) employee.assign_attributes(context.params) context.employee = employee unless employee.save context.fail!(message: "add_employee.failure") end end end class NotifyAdminAboutAdding include Interactor def call NewEmployeeNotification.send(context.employee) end end class Add include Interactor::Organizer organize CreateEmployee, NotifyAdminAboutAdding end
  37. WHY CAN’T WE SIMPLY DO THIS? def create_employee(params, organisation) employee

    = organisation.employees.new(params) return false if !employee.save NewEmployeeNotification.send(employee) employee end BETTER WAY OF DOING IT:
  38. WHY CAN’T WE SIMPLY DO THIS? class EmployeeService class <<

    self def create(params, organisation) employee = organisation.employees.new(params) return false if !employee.save NewEmployeeNotification.send(employee) employee end def update # ... end end end BETTER WAY OF DOING IT:
  39. Additional layers & building blocks Frameworks on top of Rails

    Another framework Another language Rails engines Move logic to libs Move logic to gems
  40. BEFORE_VALIDATION class Mother < Person before_validation :set_gender def set_gender self.gender

    = "F" end end mother = Mother.new mother.gender = "M" mother.valid? mother.gender # "F"
  41. CALLBACKS HELL class Issue < ActiveRecord::Base #... before_validation :default_assign, on:

    :create before_validation :clear_disabled_fields before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change, :update_closed_on after_save {|issue| issue.send :after_project_change if !issue.saved_change_to_id? && issue.saved_change_to_project_id?} after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :delete_selected_attachments, :create_journal # Should be after_create but would be called before previous after_save callbacks after_save :after_create_from_copy after_destroy :update_parent_attributes after_create :send_notification #...
  42. BEFORE_VALIDATION class Mother < Person before_validation :set_gender def set_gender self.gender

    = "F" end end mother = Mother.new mother.gender = "M" mother.valid? mother.gender # "F"
  43. BEFORE_VALIDATION → DEFAULTS class Mother < Person def initialize(attrs =

    {}) defaults = { gender: "F" } super(defaults.merge(attrs)) end end Mother.new => #<Mother id: nil, gender: "F", created_at: nil, updated_at: nil>
  44. BEFORE_VALIDATION class Room < ActiveRecord::Base belongs_to :user geocoded_by :address before_validation

    :geocode end room = Room.new room.valid? # request to external geocoding server
  45. BEFORE_VALIDATION → SERVICE class RoomsController < Web::ApplicationController def create RoomService.create(current_user,

    permitted_params) end end class RoomService class << self def create(user, params) lat, long = Geocoder.coordinates(params.address) user.rooms.build(params.merge(lat: lat, lng: long)).save! end end end
  46. BEFORE_VALIDATION → SERVICE + MUTATOR class RoomMutator class << self

    def create(user, params) user.rooms.build(params).save! end end end class RoomService class << self def create(user, params) lat, long = Geocoder.coordinates(params.address) RoomMutator.create(user, params) end end end
  47. TEXT OKAYISH CALLBACKS → GOOD CALLBACKS #TODO: move to mutator

    before_create do self.has_exercise = lesson.exercise_practice.present? self.questions_count = lesson.questions.web.count end
  48. SERVICE? class CompanyService class << self def create(user, params) company

    = Company.new(params) company.locale = user.locale company.creator = user TransactionHelper.in_transaction do company.save! member = company.members.build(user: user, role: :manager) member.accept! user.current_company_member = member user.save! company end end end end
  49. MUTATOR! class CompanyMutator class << self def create!(user, params) company

    = Company.new(params) company.locale = user.locale company.creator = user TransactionHelper.in_transaction do company.save! member = company.members.build(user: user, role: :manager) member.accept! user.current_company_member = member user.save! company end end end end
  50. NICE WAY TO DO IT: INTRODUCE COMPLEXITY HANDLERS GRADUALLY Simple

    case: - do everything in controller More complex case: - create a service/mutator Super complex scenario: - use multiple mutators inside service
  51. NICE WAY TO DO IT: INTRODUCE COMPLEXITY HANDLERS GRADUALLY def

    create_submission!(company) review = CodeReviewMutator.find_or_create_review!(run) submission = CodeReviewMutator.create_submission!(review, run) if submission.code_review.submissions.count == 1 EventSender.serve! :code_review_created, submission.code_review end CreateCodeReviewJob.perform_later(submission.id) end
  52. THE PURPOSE OF LAYERS Models treat them as domain models,

    they contain relations and business rules Mutators to handle creation/editing/deletion logic (so that we always operate with objects in correct state) Services to handle business operations logic and interaction with external services Controllers application logic (additional form fields, sessions, flash messages, redirects)
  53. TYPICAL FORM OBJECT: VIRTUS-BASED FORM-OBJECTS ⊖ doesn’t play well with

    accepts_nested_attributes_for ⊖ You have to construct the whole thing from scratch
  54. EXAMPLE SITUATION: TWO CONTEXTS Creation/editing: Article - title - body

    - author Moderation: Article - title - body - author + publication_date
  55. EXAMPLE SITUATION: RAILS-WAY SOLUTION CONDITIONAL VALIDATIONS! ⊖ pollutes model with

    a lot of conditionals ⊖ some of them are form-related conditionals
  56. CHEAPER FORM OBJECTS: PHORMS module ApplicationPhorm extend ActiveSupport::Concern module ClassMethods

    delegate :model_name, to: :superclass delegate :name, to: :superclass end end class ModerationArticlePhorm < Article include ApplicationPhorm validates :publication_date, presence: true end def update @article = ModerationArticlePhorm.find(params[:id]) if @article.update(article_params) redirect_to moderation_articles_path else render 'edit' end end
  57. CHEAPER FORM OBJECTS: PHORMS module ApplicationPhorm extend ActiveSupport::Concern module ClassMethods

    delegate :model_name, to: :superclass delegate :name, to: :superclass def permit(*args) @_args = args end def _args @_args end end def assign_attributes(attrs = {}, _options = {}) raise ArgumentError, 'expected hash' if attrs.nil? if attrs.respond_to? :permit permitted_attrs = attrs.send :permit, self.class._args super(permitted_attrs) else super(attrs) end end end bit.ly/phorm_object
  58. CHEAPER FORM OBJECTS: PHORMS ⊕ Zero maintenance cost ⊕ No

    new abstractions ⊕ Within Rails-way ⊕ Plays nicely together with controllers hierarchy ⊖ dirty hack :)
  59. TYPICAL WAY OF DOING IT: RANDOM METHODS AND FLAGS TO

    DETERMINE WHAT TO SHOW - if current_organisation_subscribed? %p You are currently subscribed, using the following payment method: %p== #{@org.card_brand}: **** **** **** #{@org.card_last4} %p== Expires #{@org.card_exp_month} / #{@org.card_exp_year} - if @org.last_charge %p== The last payment of $#{@org.last_charge.amount/100} was taken on #{@org.last_charge.created_at.strftime("%d %b %Y")} %p== The next payment of $100 is due to be taken on #{(@org.last_charge.created_at + 1.month).strftime("%d %b %Y")} = link_to "Update credit card", edit_subscription_path, class: "btn btn-primary" %p = link_to "Cancel my subscription", subscription_path, method: "DELETE", class: "btn btn-danger", data: {confirm: 'Are you sure?'} - else - if @org.trial_ended? %h2 Thank you for trialing our product %h4 To continue using our product please subscribe to the following plan: - else %h2== Thank you for trialing our product (#{distance_of_time_in_words(@org.created_at, Time.zone.now)} left) %h4 To continue using our product after trial please subscribe to the following plan: %p $100/month %p Up to 500 devices and 200 users %p 3 admin users %p CSV/XLS upload = link_to "Subscribe", new_subscription_path, class: 'btn btn-primary'
  60. TYPICAL WAY OF DOING IT: RANDOM METHODS AND FLAGS TO

    DETERMINE WHAT TO SHOW - if current_organisation_subscribed? #... - if @org.last_charge #... - elsif @org.last_charge_status != 'success' #... #... - if @org.expires_at #... - else - if @org.trial_ended? #... - else #... #...
  61. ANOTHER WAY OF DOING IT: GEMS LIKE FLAG_SHIH_TZU TO STANDARTIZE

    FLAGS class Spaceship < ActiveRecord::Base include FlagShihTzu has_flags 1 => :warpdrive, 2 => :shields, 3 => :electrolytes end Spaceship#warpdrive Spaceship#warpdrive? Spaceship#warpdrive= Spaceship#not_warpdrive Spaceship#not_warpdrive? Spaceship#not_warpdrive= Spaceship#warpdrive_changed? Spaceship#all_warpdrives Spaceship#selected_warpdrives Spaceship#select_all_warpdrives Spaceship#unselect_all_warpdrives Spaceship#selected_warpdrives= Spaceship#has_warpdrive?
  62. ANOTHER WAY OF DOING IT: GEMS LIKE FLAG_SHIH_TZU TO STANDARTIZE

    FLAGS Boolean flags are an inseparable element of most Rails apps. They control various aspects of the model business logic – from giving particular user permissions to hiding certain unfinished features. has_flags 1 => :admin, 2 => :onboarded, 3 => :can_use_mailer
  63. ANOTHER WAY OF DOING IT: GEMS LIKE FLAG_SHIH_TZU TO STANDARTIZE

    FLAGS What is wrong with it? has_flags 1 => :admin, 2 => :onboarded, 3 => :can_use_mailer
  64. WRONG WAY OF DOING IT: FLAGS CAN BE HIDDEN -

    dates - periods, - associations, - status fields - if current_organisation_subscribed? #... - if @org.last_charge #... - elsif @org.last_charge_status != 'success' #... #... - if @org.expires_at #... - else - if @org.trial_ended? #... - else #... #...
  65. BETTER WAY OF DOING IT: STATE MACHINE state_machine :state, initial:

    :trial do state :trial state :trial_ended state :subscribed state :cancelled_subscription state :not_subscribed state :payment_error event :subscribe do transition [:trial, :not_subscribed, :trial_ended] => :subscribed end event :disable_trial do transition :trial => :trial_ended end event :usage_period_ended do transition :cancelled_subscription => :not_subscribed end #... end
  66. BETTER WAY OF DOING IT: - if current_organisation.trial? #... -

    elsif current_organisation.trial_ended? #... - elsif current_organisation.subscribed? #... - elsif current_organisation.cancelled_subscription? #... - elsif current_organisation.payment_error? #... - elsif current_organisation.not_subscribed? #...
  67. BETTER WAY OF DOING IT: STATE MACHINE ⊕ Explicitly defines

    states & transitions ⊕ Prevents combinatorial explosion ⊕ You can even have multiple state machines per model
  68. TYPICAL WAY OF DOING IT: TOO MUCH LOGIC IN VIEWS

    <h3> Welcome, <% if current_user %> <%= current_user.name %> <% else %> Guest <% end %> </h3>
  69. BETTER WAY OF DOING IT: MOVE LOGIC TO CONTROLLER require

    'ostruct' helper_method :current_user def current_user @current_user ||= User.find session[:user_id] if session[:user_id] if @current_user @current_user else OpenStruct.new(name: 'Guest') end end <h3>Welcome, <%= current_user.name -%></h3>
  70. PERFECT WAY OF DOING IT: NULL OBJECTS class Guest def

    id nil end def admin? false end def moderator? false end def guest? true end def waiting_confirmation? false end def can_use_paid_features? false end def name 'Guest' end end <h3>Welcome, <%= current_user.name -%></h3> def current_user @current_user ||= User.find_by_id(id: session[:user_id]) || Guest.new end
  71. PERFECT WAY OF DOING IT: NULL OBJECTS ⊕ Explicitly define

    different types of entities in your system ⊕ You get polymorphic behavior for free ⊕ Logic-less controllers and views
  72. MIND-BLOWING WAY OF DOING IT: NULL OBJECTS class Guest include

    Nigilist def name “Guest"s end def time_zone "UTC" end def guest? true end def company OutsideInc.new end end gem nihilist class OutsideInc include Nigilist def name "Outside inc." end def trial_ended? false end end current_user = Guest.new current_user.admin? # false current_user.guest? # true current_user.address # nil current_user.time_zone # 'UTC' current_user.posts # [] current_user.company # #<OutsideInc:0x007ff105193c20>
  73. TEXT WHAT STAYS IN THE MODELS? 1) associations 2) business

    rules 3) state machines 4) defaults 5) scopes?
  74. TEXT REPOSITORIES module LikesRepository extend ActiveSupport::Concern included do scope :within_one_hour,

    -> { where('created_at > ?', 1.hour.ago) } scope :within_one_week, -> { where('created_at > ?', 1.week.ago) } scope :within_one_day, -> { where('created_at > ?', 1.day.ago) } scope :within_one_year, -> { where('created_at > ?', 1.year.ago) } end end class Like < ApplicationRecord include LikesRepository end
  75. TEXT MODEL class Organisation < ApplicationRecord has_many :devices has_many :users

    has_many :employees has_many :charges state_machine :state, initial: :trial do state :trial state :trial_ended state :subscribed state :not_subscribed event :subscribe do transition [:trial, :not_subscribed, :trial_ended] => :subscribed end event :disable_trial do transition :trial => :trial_ended end event :usage_period_ended do transition :cancelled_subscription => :not_subscribed end end def active? !users.any? end end
  76. WHAT TESTS TO WRITE? - Unit model tests? - Functional

    controller tests? - Acceptance capybara tests?
  77. TESTING RIGHT QUESTIONS 1) Useful? 2) Easy to write? 3)

    Easy to support? 4) How fragile they are? 5) How often do you need to rewrite them?
  78. TEXT CONTROLLER TESTS test "should post update" do @article =

    articles(:one) patch article_url(@article), params: {article: { title: "Supertitle", text: "Body", category_id: @secondary_category.id}} @article.reload assert { @article.title == "Supertitle" } assert { @article.state == 'draft' } assert { @article.category == @secondary_category } end test "should get edit" do get edit_article_url(articles(:one)) assert_response :success end
  79. NO STRINGS ALLOWED! 1. Always use named routes. 2. Always

    use i18n (even on single-language sites) 3. enumerize gem for enum fields 4. i18n for flash messages f(:success) f(:failure)
  80. WHY WE LOVE RSPEC? Semantics! ⊕ subject, describe, it, exepected_to,

    let, context, … ⊖ hard to structure complex tests ⊖ it_behaves_like “name" ??
  81. PRAGMATISM > AESTHETICS a % 3 != 0 it {

    is_expected.not_to be_a_multiple_of(3) } vs
  82. MINITEST IS NOT IDEAL 1) Does its job 2) Much

    faster 3) Low-level semantics. But so familiar!
  83. FIXTURES ⊕ Fast ⊕ Support cross-references ⊕ In most cases

    fixtures are more than enough ⊕ You can load them into you dev DB
  84. MOCKS 1) With stubs you replace external stuff 2) With

    mocks your tests become dependent on internal structure
  85. HOW WE MAKE OUR LIFE HARDER? 1) Easiness over simplicity

    2) Aesthetics over pragmatism 3) Popularity over thoughtful analysis 4) Conventions over criticality