Testing Drivers
Testing drivers
Section titled “Testing drivers”There are three kind of drivers
- Streaming IO (TCP, SSH, UDP, Multicast, ect)
- HTTP Client
- Logic
From a driver code structure standpoint there is no difference between these types.
- The same driver can be used over a TCP, UDP or SSH transport.
- All drivers support HTTP methods if a URI endpoint is defined.
- If a driver is associated with a System then it has access to logic helpers
During a test, the loaded module is loaded with a TCP transport, HTTP enabled and logic module capabilities. This allows for testing the full capabilities of any driver.
The driver is launched as it would be in production.
Code documentation
Section titled “Code documentation”For detailed automatically generated documentation please see the: Driver Spec API
Expectations
Section titled “Expectations”Specs have access to Crystal lang spec expectations. This allows you to confirm expectations.
variable = 34variable.should eq(34)
There is a good overview on how to use expectations on the crystal lang docs site
Status
Section titled “Status”Expectations are primarily there to test the state of the module.
- You can access state via the status helper:
status[:state_name]
- Then you can check it an expected value:
status[:state_name].should eq(14)
Testing Streaming IO
Section titled “Testing Streaming IO”The following functions are available for testing streaming IO:
transmit(data)
-> transmits the object to the module over the streaming IO interfaceresponds(data)
-> alias fortransmit
should_send(data, timeout = 500.milliseconds)
-> expects the module to respond with the data providedexpect_send(timeout = 500.milliseconds)
-> returns the nextBytes
sent by the module (useful if the data sent is not deterministic, i.e. has a time stamp)
A common test case is to ensure that module state updates as expected after transmitting some data to it:
# transmit some datatransmit(">V:2,C:11,G:2001,B:1,S:1,F:100#")
# check that the state updated as expectedstatus[:area2001].should eq(1)
Testing HTTP requests
Section titled “Testing HTTP requests”The test suite emulates a HTTP server so you can inspect HTTP requests and send canned responses to the module.
expect_http_request do |request, response| io = request.body if io data = io.gets_to_end request = JSON.parse(data) if request["message"] == "hello steve" response.status_code = 202 else response.status_code = 401 end else raise "expected request to include dialing details #{request.inspect}" endend
# check that the state updated as expectedstatus[:area2001].should eq(1)
Use expect_http_request
to access an expected request coming from the module.
- when the block completes, the response is sent to the module
- you can see
request
object details here: https://crystal-lang.org/api/latest/HTTP/Request.html - you can see
response
object details here: https://crystal-lang.org/api/latest/HTTP/Server/Response.html
Executing functions
Section titled “Executing functions”This allows you to request actions be performed in the module via the standard public interface.
exec(:function_name, argument_name: argument_value)
->response
a response future (async return value)- You should send and
responds(data)
before inspecting theresponse.get
# Execute a commandresponse = exec(:scene?, area: 1)
# Check that the command causes the module to send some datashould_send("?AREA,1,6\r\n")# Respond to that commandresponds("~AREA,1,6,2\r\n")
# Check if the functions return value is expectedresponse.get.should eq(2)# Check if the module state is correctstatus[:area1].should eq(2)
Testing Logic
Section titled “Testing Logic”Logic modules typically expect a system to contain some drivers which the logic modules interacts with.
# define mock versions of the drivers it will interact with
class Display < DriverSpecs::MockDriver include Interface::Powerable include Interface::Muteable
enum Inputs HDMI HDMI2 VGA VGA2 Miracast DVI DisplayPort HDBaseT Composite end
include PlaceOS::Driver::Interface::InputSelection(Inputs)
# Configure initial state in on_load def on_load self[:power] = false self[:input] = Inputs::HDMI end
# implement the abstract methods required by the interfaces def power(state : Bool) self[:power] = state end
def switch_to(input : Inputs) mute(false) self[:input] = input end
def mute( state : Bool = true, index : Int32 | String = 0, layer : MuteLayer = MuteLayer::AudioVideo ) self[:mute] = state self[:mute0] = state endend
Then you can define the system configuration, you can also change the system configuration throughout your spec to test different configurations.
DriverSpecs.mock_driver "Place::LogicExample" do
# Where `{Display, Display}` is referencing the `MockDriver` class defined above # and `Display:` is the friendly name # so this system would have `Display_1`, `Display_2`, `Switcher_1` system({ Display: {Display, Display}, Switcher: {Switcher}, })
# ...end
Along with the physical system configuration you can test different setting configurations. Settings can also be changed throughout the life cycle of your spec.
DriverSpecs.mock_driver "Place::LogicExample" do
settings({ name: "Meeting Room 1", map_id: "1.03" })
end
An action you perform on your driver might be expected to update state in the mock devices. You can access this state via the system
helper
DriverSpecs.mock_driver "Place::LogicExample" do
# execute a function in your logic module exec(:power, true)
# Check that the expected state has updated in you mock device system(:Display_1)[:power].should eq(true)
# manually execute a function on a mock device # need to cast the mock to the appropriate class system(:Display_1).as(Display).power falseend
All status queried in this manner is returned as a JSON::Any
object
Publishing events
Section titled “Publishing events”Emulating notifications is also possible
DriverSpecs.mock_driver "Place::LogicExample" do publish("channel/path", {payload: "data"}.to_json)end