redis_test/
lib.rs

1//! Testing support
2//!
3//! This module provides `MockRedisConnection` which implements ConnectionLike and can be
4//! used in the same place as any other type that behaves like a Redis connection. This is useful
5//! for writing unit tests without needing a Redis server.
6//!
7//! # Example
8//!
9//! ```rust
10//! use redis::{ConnectionLike, RedisError};
11//! use redis_test::{MockCmd, MockRedisConnection};
12//!
13//! fn my_exists<C: ConnectionLike>(conn: &mut C, key: &str) -> Result<bool, RedisError> {
14//!     let exists: bool = redis::cmd("EXISTS").arg(key).query(conn)?;
15//!     Ok(exists)
16//! }
17//!
18//! let mut mock_connection = MockRedisConnection::new(vec![
19//!     MockCmd::new(redis::cmd("EXISTS").arg("foo"), Ok("1")),
20//! ]);
21//!
22//! let result = my_exists(&mut mock_connection, "foo").unwrap();
23//! assert_eq!(result, true);
24//! ```
25
26pub mod cluster;
27pub mod sentinel;
28pub mod server;
29pub mod utils;
30
31use std::collections::VecDeque;
32use std::sync::{Arc, Mutex};
33
34use redis::{Cmd, ConnectionLike, ErrorKind, Pipeline, RedisError, RedisResult, Value};
35
36#[cfg(feature = "aio")]
37use futures::{future, FutureExt};
38
39#[cfg(feature = "aio")]
40use redis::{aio::ConnectionLike as AioConnectionLike, RedisFuture};
41
42/// Helper trait for converting test values into a `redis::Value` returned from a
43/// `MockRedisConnection`. This is necessary because neither `redis::types::ToRedisArgs`
44/// nor `redis::types::FromRedisValue` performs the precise conversion needed.
45pub trait IntoRedisValue {
46    /// Convert a value into `redis::Value`.
47    fn into_redis_value(self) -> Value;
48}
49
50impl IntoRedisValue for String {
51    fn into_redis_value(self) -> Value {
52        Value::BulkString(self.as_bytes().to_vec())
53    }
54}
55
56impl IntoRedisValue for &str {
57    fn into_redis_value(self) -> Value {
58        Value::BulkString(self.as_bytes().to_vec())
59    }
60}
61
62#[cfg(feature = "bytes")]
63impl IntoRedisValue for bytes::Bytes {
64    fn into_redis_value(self) -> Value {
65        Value::BulkString(self.to_vec())
66    }
67}
68
69impl IntoRedisValue for Vec<u8> {
70    fn into_redis_value(self) -> Value {
71        Value::BulkString(self)
72    }
73}
74
75impl IntoRedisValue for Value {
76    fn into_redis_value(self) -> Value {
77        self
78    }
79}
80
81impl IntoRedisValue for i64 {
82    fn into_redis_value(self) -> Value {
83        Value::Int(self)
84    }
85}
86
87/// Helper trait for converting `redis::Cmd` and `redis::Pipeline` instances into
88/// encoded byte vectors.
89pub trait IntoRedisCmdBytes {
90    /// Convert a command into an encoded byte vector.
91    fn into_redis_cmd_bytes(self) -> Vec<u8>;
92}
93
94impl IntoRedisCmdBytes for Cmd {
95    fn into_redis_cmd_bytes(self) -> Vec<u8> {
96        self.get_packed_command()
97    }
98}
99
100impl IntoRedisCmdBytes for &Cmd {
101    fn into_redis_cmd_bytes(self) -> Vec<u8> {
102        self.get_packed_command()
103    }
104}
105
106impl IntoRedisCmdBytes for &mut Cmd {
107    fn into_redis_cmd_bytes(self) -> Vec<u8> {
108        self.get_packed_command()
109    }
110}
111
112impl IntoRedisCmdBytes for Pipeline {
113    fn into_redis_cmd_bytes(self) -> Vec<u8> {
114        self.get_packed_pipeline()
115    }
116}
117
118impl IntoRedisCmdBytes for &Pipeline {
119    fn into_redis_cmd_bytes(self) -> Vec<u8> {
120        self.get_packed_pipeline()
121    }
122}
123
124impl IntoRedisCmdBytes for &mut Pipeline {
125    fn into_redis_cmd_bytes(self) -> Vec<u8> {
126        self.get_packed_pipeline()
127    }
128}
129
130/// Represents a command to be executed against a `MockConnection`.
131pub struct MockCmd {
132    cmd_bytes: Vec<u8>,
133    responses: Result<Vec<Value>, RedisError>,
134}
135
136impl MockCmd {
137    /// Create a new `MockCmd` given a Redis command and either a value convertible to
138    /// a `redis::Value` or a `RedisError`.
139    pub fn new<C, V>(cmd: C, response: Result<V, RedisError>) -> Self
140    where
141        C: IntoRedisCmdBytes,
142        V: IntoRedisValue,
143    {
144        MockCmd {
145            cmd_bytes: cmd.into_redis_cmd_bytes(),
146            responses: response.map(|r| vec![r.into_redis_value()]),
147        }
148    }
149
150    /// Create a new `MockCommand` given a Redis command/pipeline and a vector of value convertible
151    /// to a `redis::Value` or a `RedisError`.
152    pub fn with_values<C, V>(cmd: C, responses: Result<Vec<V>, RedisError>) -> Self
153    where
154        C: IntoRedisCmdBytes,
155        V: IntoRedisValue,
156    {
157        MockCmd {
158            cmd_bytes: cmd.into_redis_cmd_bytes(),
159            responses: responses.map(|xs| xs.into_iter().map(|x| x.into_redis_value()).collect()),
160        }
161    }
162}
163
164/// A mock Redis client for testing without a server. `MockRedisConnection` checks whether the
165/// client submits a specific sequence of commands and generates an error if it does not.
166#[derive(Clone)]
167pub struct MockRedisConnection {
168    commands: Arc<Mutex<VecDeque<MockCmd>>>,
169}
170
171impl MockRedisConnection {
172    /// Construct a new from the given sequence of commands.
173    pub fn new<I>(commands: I) -> Self
174    where
175        I: IntoIterator<Item = MockCmd>,
176    {
177        MockRedisConnection {
178            commands: Arc::new(Mutex::new(VecDeque::from_iter(commands))),
179        }
180    }
181}
182
183impl ConnectionLike for MockRedisConnection {
184    fn req_packed_command(&mut self, cmd: &[u8]) -> RedisResult<Value> {
185        let mut commands = self.commands.lock().unwrap();
186        let next_cmd = commands.pop_front().ok_or_else(|| {
187            RedisError::from((
188                ErrorKind::ClientError,
189                "TEST",
190                "unexpected command".to_owned(),
191            ))
192        })?;
193
194        if cmd != next_cmd.cmd_bytes {
195            return Err(RedisError::from((
196                ErrorKind::ClientError,
197                "TEST",
198                format!(
199                    "unexpected command: expected={}, actual={}",
200                    String::from_utf8(next_cmd.cmd_bytes)
201                        .unwrap_or_else(|_| "decode error".to_owned()),
202                    String::from_utf8(Vec::from(cmd)).unwrap_or_else(|_| "decode error".to_owned()),
203                ),
204            )));
205        }
206
207        next_cmd
208            .responses
209            .and_then(|values| match values.as_slice() {
210                [value] => Ok(value.clone()),
211                [] => Err(RedisError::from((
212                    ErrorKind::ClientError,
213                    "no value configured as response",
214                ))),
215                _ => Err(RedisError::from((
216                    ErrorKind::ClientError,
217                    "multiple values configured as response for command expecting a single value",
218                ))),
219            })
220    }
221
222    fn req_packed_commands(
223        &mut self,
224        cmd: &[u8],
225        _offset: usize,
226        _count: usize,
227    ) -> RedisResult<Vec<Value>> {
228        let mut commands = self.commands.lock().unwrap();
229        let next_cmd = commands.pop_front().ok_or_else(|| {
230            RedisError::from((
231                ErrorKind::ClientError,
232                "TEST",
233                "unexpected command".to_owned(),
234            ))
235        })?;
236
237        if cmd != next_cmd.cmd_bytes {
238            return Err(RedisError::from((
239                ErrorKind::ClientError,
240                "TEST",
241                format!(
242                    "unexpected command: expected={}, actual={}",
243                    String::from_utf8(next_cmd.cmd_bytes)
244                        .unwrap_or_else(|_| "decode error".to_owned()),
245                    String::from_utf8(Vec::from(cmd)).unwrap_or_else(|_| "decode error".to_owned()),
246                ),
247            )));
248        }
249
250        next_cmd.responses
251    }
252
253    fn get_db(&self) -> i64 {
254        0
255    }
256
257    fn check_connection(&mut self) -> bool {
258        true
259    }
260
261    fn is_open(&self) -> bool {
262        true
263    }
264}
265
266#[cfg(feature = "aio")]
267impl AioConnectionLike for MockRedisConnection {
268    fn req_packed_command<'a>(&'a mut self, cmd: &'a Cmd) -> RedisFuture<'a, Value> {
269        let packed_cmd = cmd.get_packed_command();
270        let response = <MockRedisConnection as ConnectionLike>::req_packed_command(
271            self,
272            packed_cmd.as_slice(),
273        );
274        future::ready(response).boxed()
275    }
276
277    fn req_packed_commands<'a>(
278        &'a mut self,
279        cmd: &'a Pipeline,
280        offset: usize,
281        count: usize,
282    ) -> RedisFuture<'a, Vec<Value>> {
283        let packed_cmd = cmd.get_packed_pipeline();
284        let response = <MockRedisConnection as ConnectionLike>::req_packed_commands(
285            self,
286            packed_cmd.as_slice(),
287            offset,
288            count,
289        );
290        future::ready(response).boxed()
291    }
292
293    fn get_db(&self) -> i64 {
294        0
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::{MockCmd, MockRedisConnection};
301    use redis::{cmd, pipe, ErrorKind, Value};
302
303    #[test]
304    fn sync_basic_test() {
305        let mut conn = MockRedisConnection::new(vec![
306            MockCmd::new(cmd("SET").arg("foo").arg(42), Ok("")),
307            MockCmd::new(cmd("GET").arg("foo"), Ok(42)),
308            MockCmd::new(cmd("SET").arg("bar").arg("foo"), Ok("")),
309            MockCmd::new(cmd("GET").arg("bar"), Ok("foo")),
310        ]);
311
312        cmd("SET").arg("foo").arg(42).exec(&mut conn).unwrap();
313        assert_eq!(cmd("GET").arg("foo").query(&mut conn), Ok(42));
314
315        cmd("SET").arg("bar").arg("foo").exec(&mut conn).unwrap();
316        assert_eq!(
317            cmd("GET").arg("bar").query(&mut conn),
318            Ok(Value::BulkString(b"foo".as_ref().into()))
319        );
320    }
321
322    #[cfg(feature = "aio")]
323    #[tokio::test]
324    async fn async_basic_test() {
325        let mut conn = MockRedisConnection::new(vec![
326            MockCmd::new(cmd("SET").arg("foo").arg(42), Ok("")),
327            MockCmd::new(cmd("GET").arg("foo"), Ok(42)),
328            MockCmd::new(cmd("SET").arg("bar").arg("foo"), Ok("")),
329            MockCmd::new(cmd("GET").arg("bar"), Ok("foo")),
330        ]);
331
332        cmd("SET")
333            .arg("foo")
334            .arg("42")
335            .exec_async(&mut conn)
336            .await
337            .unwrap();
338        let result: Result<usize, _> = cmd("GET").arg("foo").query_async(&mut conn).await;
339        assert_eq!(result, Ok(42));
340
341        cmd("SET")
342            .arg("bar")
343            .arg("foo")
344            .exec_async(&mut conn)
345            .await
346            .unwrap();
347        let result: Result<Vec<u8>, _> = cmd("GET").arg("bar").query_async(&mut conn).await;
348        assert_eq!(result.as_deref(), Ok(&b"foo"[..]));
349    }
350
351    #[test]
352    fn errors_for_unexpected_commands() {
353        let mut conn = MockRedisConnection::new(vec![
354            MockCmd::new(cmd("SET").arg("foo").arg(42), Ok("")),
355            MockCmd::new(cmd("GET").arg("foo"), Ok(42)),
356        ]);
357
358        cmd("SET").arg("foo").arg(42).exec(&mut conn).unwrap();
359        assert_eq!(cmd("GET").arg("foo").query(&mut conn), Ok(42));
360
361        let err = cmd("SET")
362            .arg("bar")
363            .arg("foo")
364            .exec(&mut conn)
365            .unwrap_err();
366        assert_eq!(err.kind(), ErrorKind::ClientError);
367        assert_eq!(err.detail(), Some("unexpected command"));
368    }
369
370    #[test]
371    fn errors_for_mismatched_commands() {
372        let mut conn = MockRedisConnection::new(vec![
373            MockCmd::new(cmd("SET").arg("foo").arg(42), Ok("")),
374            MockCmd::new(cmd("GET").arg("foo"), Ok(42)),
375            MockCmd::new(cmd("SET").arg("bar").arg("foo"), Ok("")),
376        ]);
377
378        cmd("SET").arg("foo").arg(42).exec(&mut conn).unwrap();
379        let err = cmd("SET")
380            .arg("bar")
381            .arg("foo")
382            .exec(&mut conn)
383            .unwrap_err();
384        assert_eq!(err.kind(), ErrorKind::ClientError);
385        assert!(err.detail().unwrap().contains("unexpected command"));
386    }
387
388    #[test]
389    fn pipeline_basic_test() {
390        let mut conn = MockRedisConnection::new(vec![MockCmd::with_values(
391            pipe().cmd("GET").arg("foo").cmd("GET").arg("bar"),
392            Ok(vec!["hello", "world"]),
393        )]);
394
395        let results: Vec<String> = pipe()
396            .cmd("GET")
397            .arg("foo")
398            .cmd("GET")
399            .arg("bar")
400            .query(&mut conn)
401            .expect("success");
402        assert_eq!(results, vec!["hello", "world"]);
403    }
404
405    #[test]
406    fn pipeline_atomic_test() {
407        let mut conn = MockRedisConnection::new(vec![MockCmd::with_values(
408            pipe().atomic().cmd("GET").arg("foo").cmd("GET").arg("bar"),
409            Ok(vec![Value::Array(
410                vec!["hello", "world"]
411                    .into_iter()
412                    .map(|x| Value::BulkString(x.as_bytes().into()))
413                    .collect(),
414            )]),
415        )]);
416
417        let results: Vec<String> = pipe()
418            .atomic()
419            .cmd("GET")
420            .arg("foo")
421            .cmd("GET")
422            .arg("bar")
423            .query(&mut conn)
424            .expect("success");
425        assert_eq!(results, vec!["hello", "world"]);
426    }
427}