hellostealth / stealth

An open source Ruby framework for text and voice chatbots. 🤖
https://hellostealth.org
MIT License
584 stars 57 forks source link

Integration test #209

Open josephktcheung opened 5 years ago

josephktcheung commented 5 years ago

Hi,

Related to #83, I'd like to share how I write Stealth's integration test. Source code can be found here https://github.com/josephktcheung/stealth-integration-test. @luizcarvalho @mgomes please take a look and see if this can be improved.

Steps:

  1. Run stealth new to generate a new stealth app

  2. Install following gems for testing

    group :test do
    gem "rack-test"
    gem "rspec"
    gem "mock_redis"
    end
  3. In spec/spec_helper.rb

    
    # coding: utf-8
    # frozen_string_literal: true

require 'rspec' require 'stealth' require 'mock_redis' require 'sidekiq/testing'

Requires supporting files with custom matchers and macros, etc,

in ./support/ and its subdirectories.

$LOAD_PATH.unshift(File.join(File.dirname(FILE), '..', 'bot')) $LOAD_PATH.unshift(File.join(File.dirname(FILE), '..', 'config')) $LOAD_PATH.unshift(File.dirname(FILE))

Dir["#{File.dirname(FILE)}/support/*/.rb"].each { |f| require f } require_relative "../bot/helpers/bot_helper"

ENV['STEALTH_ENV'] = 'test'

RSpec.configure do |config| I18n.load_path += Dir[File.join(File.dirname(FILE), '..', 'config', 'locales', '*.{rb,yml}')] config.include BotHelper config.before(:each) do |example| Sidekiq::Worker.clear_all Sidekiq::Testing.fake! $redis = MockRedis.new allow(Redis).to receive(:new).and_return($redis) end config.filter_run_when_matching :focus config.formatter = :documentation

config.before(:suite) do Stealth.boot end end


4. Define `sample_message` class in `spec/support/sample_message.rb`
```ruby
class SampleMessage

  def initialize(service:)
    @service = service
    @base_message = Stealth::ServiceMessage.new(service: @service)
    @base_message.sender_id = sender_id
    @base_message.timestamp = timestamp
    @base_message
  end

  def message_with_text(message)
    @base_message.message = message
    self
  end

  def message_with_payload(payload)
    @base_message.payload = payload
    self
  end

  def message_with_location(location)
    @base_message.location = location
    self
  end

  def message_with_attachments(attachments)
    @base_message.attachments = attachments
    self
  end

  def sender_id
    "8b3e0a3c-62f1-401e-8b0f-615c9d256b1f"
  end

  def timestamp
    @base_message.timestamp || Time.now
  end

  def to_request_json
    if @base_message.message.present?
      JSON.generate({
        entry: [
          {
            "messaging": [
              "sender": {
                "id": @base_message.sender_id
              },
              "recipient": {
                "id": "<PAGE_ID>"
              },
              "timestamp": @base_message.timestamp.to_i * 1000,
              "message": {
                "mid":"mid.1457764197618:41d102a3e1ae206a38",
                "text": @base_message.message
              }
            ]
          }
        ]
      })
    end
  end
end
  1. Define custom matcher send_reply in spec/support/matchers/send_reply.rb (Thanks @sunny for correction)

    RSpec::Matchers.define :receive_message do |message|
    match do |client|
    stub = double("client")
    allow(stub).to receive(:transmit).and_return(true)
    @replies.each do |reply|
      expect(client).to receive(:new)
        .with(hash_including(reply: hash_including(reply)))
        .ordered
        .and_return(stub)
    end
    
    json = message.to_request_json
    post "/incoming/#{@service}", json, { "CONTENT_TYPE" => "application/json" }
    end
    
    chain :as_service do |service|
    @service = service
    end
    
    chain :and_send_replies do |replies|
    @replies = replies
    end
    end
  2. In spec/features/chatbot_flow_spec.rb

    
    require "spec_helper"

describe "chatbot flow" do include Rack::Test::Methods

def app Stealth::Server end

let(:message) { SampleMessage.new( service: "facebook" ) }

let(:client) { Stealth::Services::Facebook::Client }

it "handles user conversation" do Sidekiq::Testing.inline! do expect(client).to receive_message( message.message_with_text("hello") ) .as_service("facebook") .and_send_replies([ { "recipient" => { "id" => message.sender_id }, "message" => { "text" => "Hello World!" } }, { "recipient" => { "id" => message.sender_id }, "message" => { "text" => "Goodbye World!" } } ]) end end end


7. In `bot/controllers/hellos_controller.rb`
```ruby
class HellosController < BotController

  def say_hello
    send_replies
    step_to flow: "goodbye"
  end

end
  1. Run bundle e rspec and test passes
josephktcheung commented 5 years ago

And we can chain multi-step conversation like this:

expect(client).to receive_message(
  message.message_with_text("hello")
)
  .as_service("facebook")
  .and_send_replies([
    {
      "recipient" => {
        "id" => message.sender_id
      },
      "message" => {
        "text" => "Hello World!"
      }
    },
    {
      "recipient" => {
        "id" => message.sender_id
      },
      "message" => {
        "text" => "What's your name?"
      }
    }
  ])

expect(client).to receive_message(
  message.message_with_text("Luke Skywalker")
)
  .as_service("facebook")
  .and_send_replies([
    {
      "recipient" => {
        "id" => message.sender_id
      },
      "message" => {
        "text" => "Nice to meet you Luke Skywalker!"
      }
    },
    {
      "recipient" => {
        "id" => message.sender_id
      },
      "message" => {
        "text" => "Goodbye World!"
      }
    }
  ])
sunny commented 4 years ago

👏

Thanks for sharing this! This needs a page in the docs, perhaps?

To load spec/matchers/send_reply.rb, spec_helper.rb probably also needs:

Dir["#{File.dirname(__FILE__)}/matchers/**/*.rb"].each { |f| require f }

Also, is the JSON from SampleMessage specific to Facebook's webhook?

rahulkeerthi commented 4 years ago

Hi @sunny - @josephktcheung's matchers folder is in the support folder so not needed in this case as it's loaded via the ** wildcard. You can see the folder structure here.

I'm just playing around right now and I think JSON is Facebook-specific as Twilio webhooks use TwiML (its own flavour of XML) via the twilio-ruby gem.

Also hello fellow LWer 👋

sunny commented 4 years ago

Hey @rahulkeerthi, great to see more people from the Le Wagon family :)

matchers folder is in the support folder so not needed in this case

Ah, thanks! I followed this issue's description, it may need a little fix, then:

-4. Define custom matcher `send_reply` in `spec/matchers/send_reply.rb`
+4. Define custom matcher `send_reply` in `spec/support/matchers/send_reply.rb`
rahulkeerthi commented 4 years ago

I followed this issue's description

Ah, I missed that - you're right! 👍

gorandev commented 1 year ago

Hi @josephktcheung, I'm trying to implement this but it won't work, I'm getting

     Failure/Error:
       expect(client).to receive(:new)
         .with(hash_including(reply: hash_including(reply)))
         .ordered
         .and_return(stub)

       (Stealth::Services::Facebook::Client (class)).new(hash_including(:reply=>"hash_including(\"recipient\"=>{\"id\"=>\"8b3e0a3c-62f1-401e-8b0f-615c9d256b1f\"}, \"message\"=>{\"text\"=>\"Hello World!\"})"))
           expected: 1 time with arguments: (hash_including(:reply=>"hash_including(\"recipient\"=>{\"id\"=>\"8b3e0a3c-62f1-401e-8b0f-615c9d256b1f\"}, \"message\"=>{\"text\"=>\"Hello World!\"})"))
           received: 0 times
     # ./spec/support/matchers/send_reply.rb:6:in `block (3 levels) in <top (required)>'

I'm looking into the source code for Stealth and Stealth::Facebook to figure out what might have changed between when you wrote this and now, and I was wondering if you were still around?

Thank you for reading! Will post back here if I solve it on my own. :-)