best practice in development
TRANSCRIPT
Best Practice in developmentCDD, MVC principles, Rails hints
1 / 27
Our WorkflowComment Driven Development
1. write integration test in comments
2. rewrite comments to excecutable integration test
3. implement basic test infrastrucuture
4. think about architecture and design
5. implement object using TDD
2 / 27
NeutrinoHandling bounced emails using Amazon SNS
Background
application for sending promo emailssimilar to MailChimp
emails are send by Amazon::SESwe have to handle bounce emails due to our reputation in Amazonwe desided to implement it using Amazon::SNSwe have to handle request with json data
3 / 27
Write integration test in commentsrequire "spec_helper"
describe "Amazon bounce handling process" do # We have subscriptions on email "[email protected]" in different clients # When last email to him was returned with "permanent failure" # Then all subscription on email "[email protected]" are marked as bouncedend
rewrite task to commentsyou will ensure that you understand what you are suppose to docomments can be send to project manager to ensure task was understoodproperlychanges on this level cost you nothingsomething like cucumber, but without parsing
4 / 27
Rewrite comments to parsable testdescribe "Amazon bounce handling process" do it "marks all subcriptions as bounced on permanent failure notifications" do # Given different_active_subscription_on("[email protected]")
# When receive_permanent_failure_notification(on_receiver: "[email protected]")
# Then all_subscriptions_on("[email protected]").should be_bounced endend
just get comments to some ruby parseable formyou express all you customed matchestest should be as readable as possible
5 / 27
Implement test infrastrucutureconverts error test to failing test
implement all helper methods and custom matchesimplement basic application infrastrucutreat the end you have failing test
matcher :be_bounced do match do |actual_subscriptions| actual_subscriptions.all? do |subscription| subscription.status == "bounced" end endend
6 / 27
def different_active_subscription_on(email) subscriber_1 = create(:subscriber, email: email, client: create(:client)) subscriber_2 = create(:subscriber, email: email, client: create(:client)) # ... subscription1, subscription2end
def receive_permanent_failure_notification(options) post "/amazon_sns/bounces", permanent_failure_raw_data(options), { "CONTENT_TYPE" => 'text/plain' }end
def permanent_failure_raw_data(options) '{ "notificationType":"Bounce",' # ....end
namespace :amazon_sns do post "bounces", :to => "bounce_listeners#handle"end
class AmazonSns::BounceListenersController < ApplicationController def handle render :nothing => true endend
7 / 27
Think about architecture and designclass AmazonSns::BounceListenersController < ApplicationController def handle bounce_object = bounce_parser(request_data) bounce_handler.handle(bounce_object)
# bounce_handler bounce_object.emails.each do |email| subscription_marker.mark_as_bounced(email) end
render :nothing => true endend
you think how you wish to have object communicationas least conditions as possibledesigning object messenging is as writing test
8 / 27
Implement object using TDDnow you know what methods and objects do you needjust implement them in TTD style
�rst write unitestthen write implementationif you found test hard to write you should once again think aboutarchitecture
at the end you should have all tests (unit, integration) in green
9 / 27
How do we codeskinny controller, skinny model as wellbusiness logic in separate classes
avoid callbacks in model to send emailORM is just input / output layerextract simple pure ruby objects (puro)keep views simple & stupid (kiss)
10 / 27
How do we testmodels (active record classes) are tested in integration testsdata objects are tested using unit testsservice objects are tested using mocks, to test object interactioncontrollers do not have special testshole stack (user interface, application, database, etc.) is covert by integrationtests
11 / 27
UniligaShowcase of code samples
Background
small portal for sport players in Bratislavarandom players are put into the groups according to their skills, then theyplay matches with each otheraccording to their results, they go to upper or bottom league
sample is from season ticket ordering partplayer can order ticket from their pro�leplayer is noti�ed by email
12 / 27
Skinny Controllers def create @season_ticket = SeasonTicket.new(params[:season_ticket]) @season_ticket.season = SeasonManager.for_ordering @season_ticket.player = current_customer
@notifiers = [ MailerNotifier.new(SeasonTicketMailer, :ordered_notification) ]
if @season_ticket.valid? SeasonTicketOrderer.new(@season_ticket, @notifiers).order! notice_message redirect_to wnm_core.profile_path else # ... end end
only creates objectsall if branches are covered in integration test
13 / 27
Service objectclass SeasonTicketOrderer def initialize(ticket, notifiers = []) @ticket, @notifiers = ticket, notifiers end
def order! @ticket.order! @notifiers.each{ |notifier| notifier.notify(@ticket) } endend
class SeasonTicketMailer < ActionMailer::Base def ordered_notification(ticket) @ticket = ticket mail :to => @ticket.player_email, :subject => t("mailers.season...") end
MailerNotifier.new(SeasonTicketMailer, :ordered_notification)
no coupling between model and emailany other noti�er can by passed to orderer class
14 / 27
ORM as output targetclass SeasonTicket < ActiveRecord::Base def order! self.price = calculate_price self.status = :ordered save! self end
class Group < ActiveRecord::Base def self.create_with_players(number, players, league, round) create!( :sequence => number, :league_id => league.id, :round_id => round.id, :players => players ) end
subs, mocks in services classes are easycreates thin wrapper around active record
15 / 27
ORM as input sourceclass SeasonTicket < ActiveRecord::Base # ... scope :payed, lambda{ where(:status => "payed") } scope :for_sport_region, lambda{ |s_region_id| where(:sport_region_id => s_region_id) }
def self.payed_for_region_season(region, season) payed.for_region_season(region, season) end
def self.for_region_season(region, season) for_sport_region(region).for_season(season) end # ... end
select index are clearly visiblesubs, mocks in services classes are easy
16 / 27
Extract PURE from modelclass SeasonTicket < ActiveRecord::Base # ... def due_date @due_date ||= DueDateCalculator.new(season).calculate endend
class DueDateCalculator def initialize(season) @season = season end
def calculate @season.upcoming_round.start_date - 1.day endend
unit tests for DueDateCalculator do not have to load railssingle responsibility principle for object classes
17 / 27
Use exceptionclass SeasonManager NoSeason = Class.new(StandardError)
def self.active Season.active end
def self.upcoming Season.upcoming end
def self.for_ordering if active && active.upcoming_round? active elsif upcoming upcoming else raise SeasonManager::NoSeason.new( "we are missing upcoming season, create one in admin" ) end endend
18 / 27
Handle exception properlyclass SeasonTicketsController < ApplicationController rescue_from SeasonManager::NoSeason, :with => :no_season_apologize
def create @season_ticket = SeasonTicket.new(params[:season_ticket]) # this call can raise SeasonManager::NoSeason @season_ticket.season = SeasonManager.for_ordering # ... end
def no_season_apologize flash[:alert] = t("season_tickets.messages.no_season_apologize.alert") redirect_to main_app.profile_path endend
19 / 27
Views Principleskeep it simple and stupid
no logicnot deep object chaningtest in emails do not create database objectsuse objects to simplify views
20 / 27
Bad patternsTied couplingclass Shop < ActiveRecord::Base after_save :when_tags_changed
def when_shop_created if id_changed? ShopMailer.shop_created(self).deliver end endend
solve with service class
21 / 27
Callback hell
solve with PURE and service objects
class Shop < ActiveRecord::Base before_validation :uniform_shop_url, :fill_in_catalog_name, :add_url_protocol before_save :update_service_level, :create_shop_user, :when_ppc_daily_limit_changed after_save :when_tags_changed, :when_xml_changed, :when_shop_changed, :when_shop_created after_destroy :clean_shop_leftovers, :recache_shop_categories, :add_category_url_to_history
def uniform_shop_url @invalid_url = false return unless url.present?
local_url = url.strip local_url = local_url[0..-2] if local_url[-1] == '/' if local_url[0..6] != "http://" && local_url[0..7] != "https://" local_url = "http://"+local_url end
uri = Addressable::URI.parse(local_url) # ... rescue Addressable::URI::InvalidURIError @invalid_url = true endend
22 / 27
it will workclass GalleryItem < ActiveRecord::Base def upload_thumbnail_from_video vid = YouTubeIt::Client.new.video_by(video_url) self.remote_image_url = vid.thumbnails.first save! endend
solve with thin wrapper VideoThumbnailGrabbertest wrapper in integrationthen you can stub your wrapper
describe "upload_thumbnail_from_video" do it "should set thumbnail from youtube if video was changed, not blank, and image was not changed" gi = GalleryItem.new
YouTubeIt::Client.any_instance.should_receive(:video_by) .with("http://www.youtube.com/watch?v=Nba3Tr_GLZU").and_return(double()
gi.video_url = "http://www.youtube.com/watch?v=Nba3Tr_GLZU" gi.upload_thumbnail_from_video.should be_true endend
23 / 27
heavy view
solve with null object, methods, new objects
<% if profile.add_to_shadowbox?(profile.payed_subcategory_id(@category_id) || @category_subtree_ids) %> <%= link_to profile.gallery_item_opened_url( profile.payed_subcategory_id(@category_id) || @category_subtree_ids), :title => profile.gallery_item(profile.payed_subcategory_id(@category_id) || <%= image_tag(profile.gallery_item_thumb_url( profile.payed_subcategory_id(@category_id) || @category_subtree_ids), :alt => profile.name.to_s) %> <% end %><% end %>
24 / 27
<% if profile.has_image_or_video? %> <%= link_to profile.gallery_item_zoom_url, :rel => gallery_name do %> <%= image_tag(profile.gallery_item_thumb_url, :alt => profile.name.to_s) %> <% end %><% end %>
class ProfilePresenter attr_reader :profile, :category
delegate :id, :name, :phone, :email, :url, :link, :to => :profile delegate :has_video?, :has_image_or_video?, :to => :gallery_item delegate :zoom_url, :thumb_url, :description, :to => :gallery_item, :prefix =>
def initialize(profile, category) @profile = profile @category = category end
def gallery_item @gallery_item ||= profile.gallery_items_ordered(category).first || NullGalleryItem.new endend
25 / 27
Why ?easier maintenance: lots of small objects with one responsibility are betterthan one big object with a lot of code and a lot of responsibilitybetter architecture [not sure about what you want to say here]fewer bugsinterfaces (object borders) are well de�nedit is much easier to swap some object for another if I want to changesomethingonly few request tests are neededunit testing is much easiertests that do not load the whole Rails stack are extremely fast
26 / 27
Referenceshttps://www.destroyallsoftware.com/screencastshttp://www.amazon.com/Growing-Object-Oriented-Software-Guided-Tests/dp/0321503627http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/http://www.confreaks.com/videos/1314-rubyconf2012-boundarieshttp://www.confreaks.com/videos/759-rubymidwest2011-keynote-architecture-the-lost-years
27 / 27