or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

basic-effects.mdchannels.mdconcurrency-effects.mdhelper-effects.mdindex.mdmiddleware.mdtesting.mdutilities.md

testing.mddocs/

0

# Testing

1

2

Tools for testing sagas in isolation, including cloneable generators and mock tasks. These utilities make it easy to test saga logic without running the full Redux-Saga middleware.

3

4

## Capabilities

5

6

### cloneableGenerator

7

8

Creates a cloneable generator from a saga function. This allows testing different branches of saga execution without replaying all the previous steps.

9

10

```typescript { .api }

11

/**

12

* Create cloneable generator for testing different saga branches

13

* @param saga - Generator function to make cloneable

14

* @returns Function that creates cloneable generator instances

15

*/

16

function cloneableGenerator<S extends Saga>(

17

saga: S

18

): (...args: Parameters<S>) => SagaIteratorClone;

19

20

interface SagaIteratorClone extends SagaIterator {

21

/** Create clone at current execution point */

22

clone(): SagaIteratorClone;

23

}

24

```

25

26

**Usage Examples:**

27

28

```typescript

29

import { cloneableGenerator } from "@redux-saga/testing-utils";

30

import { call, put, select } from "redux-saga/effects";

31

32

function* userSaga(action) {

33

const userId = action.payload.userId;

34

const user = yield call(api.fetchUser, userId);

35

36

const isAdmin = yield select(getIsAdmin, user.id);

37

38

if (isAdmin) {

39

yield put({ type: 'ADMIN_USER_LOADED', user });

40

} else {

41

yield put({ type: 'REGULAR_USER_LOADED', user });

42

}

43

}

44

45

// Test different branches without replaying setup

46

describe('userSaga', () => {

47

it('handles admin and regular users', () => {

48

const generator = cloneableGenerator(userSaga)({

49

payload: { userId: 123 }

50

});

51

52

// Common setup

53

assert.deepEqual(

54

generator.next().value,

55

call(api.fetchUser, 123)

56

);

57

58

const mockUser = { id: 123, name: 'John' };

59

assert.deepEqual(

60

generator.next(mockUser).value,

61

select(getIsAdmin, 123)

62

);

63

64

// Clone at decision point

65

const adminClone = generator.clone();

66

const regularClone = generator.clone();

67

68

// Test admin branch

69

assert.deepEqual(

70

adminClone.next(true).value,

71

put({ type: 'ADMIN_USER_LOADED', user: mockUser })

72

);

73

74

// Test regular user branch

75

assert.deepEqual(

76

regularClone.next(false).value,

77

put({ type: 'REGULAR_USER_LOADED', user: mockUser })

78

);

79

});

80

});

81

```

82

83

### createMockTask

84

85

Creates a mock task object for testing saga interactions with forked tasks. The mock task can be configured to simulate different task states and results.

86

87

```typescript { .api }

88

/**

89

* Create mock task for testing saga task interactions

90

* @returns MockTask that can be configured for different scenarios

91

*/

92

function createMockTask(): MockTask;

93

94

interface MockTask extends Task {

95

/** @deprecated Use setResult, setError, or cancel instead */

96

setRunning(running: boolean): void;

97

/** Set successful result for the task */

98

setResult(result: any): void;

99

/** Set error result for the task */

100

setError(error: any): void;

101

/** Standard task methods */

102

isRunning(): boolean;

103

result<T = any>(): T | undefined;

104

error(): any | undefined;

105

toPromise<T = any>(): Promise<T>;

106

cancel(): void;

107

setContext(props: object): void;

108

}

109

```

110

111

**Usage Examples:**

112

113

```typescript

114

import { createMockTask } from "@redux-saga/testing-utils";

115

import { fork, join, cancel } from "redux-saga/effects";

116

117

function* coordinatorSaga() {

118

const task1 = yield fork(workerSaga1);

119

const task2 = yield fork(workerSaga2);

120

121

try {

122

const results = yield join([task1, task2]);

123

yield put({ type: 'ALL_TASKS_COMPLETED', results });

124

} catch (error) {

125

yield cancel([task1, task2]);

126

yield put({ type: 'TASKS_FAILED', error });

127

}

128

}

129

130

describe('coordinatorSaga', () => {

131

it('handles successful task completion', () => {

132

const generator = coordinatorSaga();

133

134

// Mock the fork effects

135

generator.next(); // first fork

136

generator.next(); // second fork

137

138

// Create mock tasks

139

const mockTask1 = createMockTask();

140

const mockTask2 = createMockTask();

141

142

// Set successful results

143

mockTask1.setResult('result1');

144

mockTask2.setResult('result2');

145

146

// Test join behavior

147

const joinEffect = generator.next([mockTask1, mockTask2]).value;

148

assert.deepEqual(joinEffect, join([mockTask1, mockTask2]));

149

150

// Verify final action

151

const putEffect = generator.next(['result1', 'result2']).value;

152

assert.deepEqual(putEffect, put({

153

type: 'ALL_TASKS_COMPLETED',

154

results: ['result1', 'result2']

155

}));

156

});

157

158

it('handles task failures', () => {

159

const generator = coordinatorSaga();

160

161

generator.next(); // first fork

162

generator.next(); // second fork

163

164

const mockTask1 = createMockTask();

165

const mockTask2 = createMockTask();

166

167

// Set error on one task

168

mockTask1.setError(new Error('Task failed'));

169

170

// Test error handling

171

const error = new Error('Task failed');

172

const cancelEffect = generator.throw(error).value;

173

174

assert.deepEqual(cancelEffect, cancel([mockTask1, mockTask2]));

175

});

176

});

177

```

178

179

## Testing Patterns

180

181

### Basic Saga Testing

182

183

```typescript

184

import { call, put } from "redux-saga/effects";

185

186

function* fetchUserSaga(action) {

187

try {

188

const user = yield call(api.fetchUser, action.payload.id);

189

yield put({ type: 'FETCH_USER_SUCCESS', user });

190

} catch (error) {

191

yield put({ type: 'FETCH_USER_FAILURE', error: error.message });

192

}

193

}

194

195

describe('fetchUserSaga', () => {

196

it('fetches user successfully', () => {

197

const action = { payload: { id: 1 } };

198

const generator = fetchUserSaga(action);

199

200

// Test API call

201

assert.deepEqual(

202

generator.next().value,

203

call(api.fetchUser, 1)

204

);

205

206

// Test success action

207

const user = { id: 1, name: 'John' };

208

assert.deepEqual(

209

generator.next(user).value,

210

put({ type: 'FETCH_USER_SUCCESS', user })

211

);

212

213

// Test completion

214

assert.equal(generator.next().done, true);

215

});

216

217

it('handles API errors', () => {

218

const action = { payload: { id: 1 } };

219

const generator = fetchUserSaga(action);

220

221

generator.next(); // Skip to after call

222

223

// Throw error

224

const error = new Error('API Error');

225

assert.deepEqual(

226

generator.throw(error).value,

227

put({ type: 'FETCH_USER_FAILURE', error: 'API Error' })

228

);

229

});

230

});

231

```

232

233

### Testing with runSaga

234

235

```typescript

236

import { runSaga } from "redux-saga";

237

238

describe('saga integration tests', () => {

239

it('dispatches correct actions', async () => {

240

const dispatched = [];

241

const mockStore = {

242

getState: () => ({ user: { id: 1 } }),

243

dispatch: (action) => dispatched.push(action)

244

};

245

246

await runSaga(mockStore, fetchUserSaga, { payload: { id: 1 } }).toPromise();

247

248

expect(dispatched).toContainEqual({

249

type: 'FETCH_USER_SUCCESS',

250

user: { id: 1, name: 'John' }

251

});

252

});

253

});

254

```

255

256

### Testing Channels

257

258

```typescript

259

import { channel } from "redux-saga";

260

import { take, put } from "redux-saga/effects";

261

262

function* channelSaga() {

263

const chan = yield call(channel);

264

yield fork(producer, chan);

265

yield fork(consumer, chan);

266

}

267

268

describe('channelSaga', () => {

269

it('creates and uses channel', () => {

270

const generator = channelSaga();

271

272

// Test channel creation

273

assert.deepEqual(

274

generator.next().value,

275

call(channel)

276

);

277

278

// Mock channel

279

const mockChannel = { put: jest.fn(), take: jest.fn() };

280

281

// Test producer fork

282

assert.deepEqual(

283

generator.next(mockChannel).value,

284

fork(producer, mockChannel)

285

);

286

287

// Test consumer fork

288

assert.deepEqual(

289

generator.next().value,

290

fork(consumer, mockChannel)

291

);

292

});

293

});

294

```

295

296

### Testing Error Boundaries

297

298

```typescript

299

function* sagaWithErrorHandling() {

300

try {

301

yield call(riskyOperation);

302

yield put({ type: 'SUCCESS' });

303

} catch (error) {

304

yield put({ type: 'ERROR', error: error.message });

305

} finally {

306

yield put({ type: 'CLEANUP' });

307

}

308

}

309

310

describe('error handling', () => {

311

it('handles errors and runs cleanup', () => {

312

const generator = sagaWithErrorHandling();

313

314

generator.next(); // Skip to call

315

316

// Throw error

317

const error = new Error('Operation failed');

318

const errorAction = generator.throw(error).value;

319

assert.deepEqual(errorAction, put({

320

type: 'ERROR',

321

error: 'Operation failed'

322

}));

323

324

// Test finally block

325

const cleanupAction = generator.next().value;

326

assert.deepEqual(cleanupAction, put({ type: 'CLEANUP' }));

327

});

328

});

329

```

330

331

## Testing Best Practices

332

333

1. **Test individual effects**: Verify each yielded effect matches expected values

334

2. **Use cloneableGenerator**: Test different branches without duplicating setup

335

3. **Mock external dependencies**: Use mock tasks and channels for isolation

336

4. **Test error paths**: Verify error handling using generator.throw()

337

5. **Test integration**: Use runSaga for end-to-end saga testing

338

6. **Assert completion**: Check generator.next().done for saga completion