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((ErrorKind::Client, "TEST", "unexpected command".to_owned()))
188        })?;
189
190        if cmd != next_cmd.cmd_bytes {
191            return Err(RedisError::from((
192                ErrorKind::Client,
193                "TEST",
194                format!(
195                    "unexpected command: expected={}, actual={}",
196                    String::from_utf8(next_cmd.cmd_bytes)
197                        .unwrap_or_else(|_| "decode error".to_owned()),
198                    String::from_utf8(Vec::from(cmd)).unwrap_or_else(|_| "decode error".to_owned()),
199                ),
200            )));
201        }
202
203        next_cmd
204            .responses
205            .and_then(|values| match values.as_slice() {
206                [value] => Ok(value.clone()),
207                [] => Err(RedisError::from((
208                    ErrorKind::Client,
209                    "no value configured as response",
210                ))),
211                _ => Err(RedisError::from((
212                    ErrorKind::Client,
213                    "multiple values configured as response for command expecting a single value",
214                ))),
215            })
216    }
217
218    fn req_packed_commands(
219        &mut self,
220        cmd: &[u8],
221        _offset: usize,
222        _count: usize,
223    ) -> RedisResult<Vec<Value>> {
224        let mut commands = self.commands.lock().unwrap();
225        let next_cmd = commands.pop_front().ok_or_else(|| {
226            RedisError::from((ErrorKind::Client, "TEST", "unexpected command".to_owned()))
227        })?;
228
229        if cmd != next_cmd.cmd_bytes {
230            return Err(RedisError::from((
231                ErrorKind::Client,
232                "TEST",
233                format!(
234                    "unexpected command: expected={}, actual={}",
235                    String::from_utf8(next_cmd.cmd_bytes)
236                        .unwrap_or_else(|_| "decode error".to_owned()),
237                    String::from_utf8(Vec::from(cmd)).unwrap_or_else(|_| "decode error".to_owned()),
238                ),
239            )));
240        }
241
242        next_cmd.responses
243    }
244
245    fn get_db(&self) -> i64 {
246        0
247    }
248
249    fn check_connection(&mut self) -> bool {
250        true
251    }
252
253    fn is_open(&self) -> bool {
254        true
255    }
256}
257
258#[cfg(feature = "aio")]
259impl AioConnectionLike for MockRedisConnection {
260    fn req_packed_command<'a>(&'a mut self, cmd: &'a Cmd) -> RedisFuture<'a, Value> {
261        let packed_cmd = cmd.get_packed_command();
262        let response = <MockRedisConnection as ConnectionLike>::req_packed_command(
263            self,
264            packed_cmd.as_slice(),
265        );
266        future::ready(response).boxed()
267    }
268
269    fn req_packed_commands<'a>(
270        &'a mut self,
271        cmd: &'a Pipeline,
272        offset: usize,
273        count: usize,
274    ) -> RedisFuture<'a, Vec<Value>> {
275        let packed_cmd = cmd.get_packed_pipeline();
276        let response = <MockRedisConnection as ConnectionLike>::req_packed_commands(
277            self,
278            packed_cmd.as_slice(),
279            offset,
280            count,
281        );
282        future::ready(response).boxed()
283    }
284
285    fn get_db(&self) -> i64 {
286        0
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::{MockCmd, MockRedisConnection};
293    use redis::{cmd, pipe, ErrorKind, Value};
294
295    #[test]
296    fn sync_basic_test() {
297        let mut conn = MockRedisConnection::new(vec![
298            MockCmd::new(cmd("SET").arg("foo").arg(42), Ok("")),
299            MockCmd::new(cmd("GET").arg("foo"), Ok(42)),
300            MockCmd::new(cmd("SET").arg("bar").arg("foo"), Ok("")),
301            MockCmd::new(cmd("GET").arg("bar"), Ok("foo")),
302        ]);
303
304        cmd("SET").arg("foo").arg(42).exec(&mut conn).unwrap();
305        assert_eq!(cmd("GET").arg("foo").query(&mut conn), Ok(42));
306
307        cmd("SET").arg("bar").arg("foo").exec(&mut conn).unwrap();
308        assert_eq!(
309            cmd("GET").arg("bar").query(&mut conn),
310            Ok(Value::BulkString(b"foo".as_ref().into()))
311        );
312    }
313
314    #[cfg(feature = "aio")]
315    #[tokio::test]
316    async fn async_basic_test() {
317        let mut conn = MockRedisConnection::new(vec![
318            MockCmd::new(cmd("SET").arg("foo").arg(42), Ok("")),
319            MockCmd::new(cmd("GET").arg("foo"), Ok(42)),
320            MockCmd::new(cmd("SET").arg("bar").arg("foo"), Ok("")),
321            MockCmd::new(cmd("GET").arg("bar"), Ok("foo")),
322        ]);
323
324        cmd("SET")
325            .arg("foo")
326            .arg("42")
327            .exec_async(&mut conn)
328            .await
329            .unwrap();
330        let result: Result<usize, _> = cmd("GET").arg("foo").query_async(&mut conn).await;
331        assert_eq!(result, Ok(42));
332
333        cmd("SET")
334            .arg("bar")
335            .arg("foo")
336            .exec_async(&mut conn)
337            .await
338            .unwrap();
339        let result: Result<Vec<u8>, _> = cmd("GET").arg("bar").query_async(&mut conn).await;
340        assert_eq!(result.as_deref(), Ok(&b"foo"[..]));
341    }
342
343    #[test]
344    fn errors_for_unexpected_commands() {
345        let mut conn = MockRedisConnection::new(vec![
346            MockCmd::new(cmd("SET").arg("foo").arg(42), Ok("")),
347            MockCmd::new(cmd("GET").arg("foo"), Ok(42)),
348        ]);
349
350        cmd("SET").arg("foo").arg(42).exec(&mut conn).unwrap();
351        assert_eq!(cmd("GET").arg("foo").query(&mut conn), Ok(42));
352
353        let err = cmd("SET")
354            .arg("bar")
355            .arg("foo")
356            .exec(&mut conn)
357            .unwrap_err();
358        assert_eq!(err.kind(), ErrorKind::Client);
359        assert_eq!(err.detail(), Some("unexpected command"));
360    }
361
362    #[test]
363    fn errors_for_mismatched_commands() {
364        let mut conn = MockRedisConnection::new(vec![
365            MockCmd::new(cmd("SET").arg("foo").arg(42), Ok("")),
366            MockCmd::new(cmd("GET").arg("foo"), Ok(42)),
367            MockCmd::new(cmd("SET").arg("bar").arg("foo"), Ok("")),
368        ]);
369
370        cmd("SET").arg("foo").arg(42).exec(&mut conn).unwrap();
371        let err = cmd("SET")
372            .arg("bar")
373            .arg("foo")
374            .exec(&mut conn)
375            .unwrap_err();
376        assert_eq!(err.kind(), ErrorKind::Client);
377        assert!(err.detail().unwrap().contains("unexpected command"));
378    }
379
380    #[test]
381    fn pipeline_basic_test() {
382        let mut conn = MockRedisConnection::new(vec![MockCmd::with_values(
383            pipe().cmd("GET").arg("foo").cmd("GET").arg("bar"),
384            Ok(vec!["hello", "world"]),
385        )]);
386
387        let results: Vec<String> = pipe()
388            .cmd("GET")
389            .arg("foo")
390            .cmd("GET")
391            .arg("bar")
392            .query(&mut conn)
393            .expect("success");
394        assert_eq!(results, vec!["hello", "world"]);
395    }
396
397    #[test]
398    fn pipeline_atomic_test() {
399        let mut conn = MockRedisConnection::new(vec![MockCmd::with_values(
400            pipe().atomic().cmd("GET").arg("foo").cmd("GET").arg("bar"),
401            Ok(vec![Value::Array(
402                vec!["hello", "world"]
403                    .into_iter()
404                    .map(|x| Value::BulkString(x.as_bytes().into()))
405                    .collect(),
406            )]),
407        )]);
408
409        let results: Vec<String> = pipe()
410            .atomic()
411            .cmd("GET")
412            .arg("foo")
413            .cmd("GET")
414            .arg("bar")
415            .query(&mut conn)
416            .expect("success");
417        assert_eq!(results, vec!["hello", "world"]);
418    }
419}