In the previous chapter, you learned how to use Firebase Crashlytics to efficiently track errors in your app. This knowledge plays an enormous role when resolving the existing issues your users face. However, the experience for your users would be even better if the app didn’t have those issues in the first place. It’s delusional to believe your app will be bug-free with your first release, but you still try to get as close to this ideal scenario as possible. You can achieve this with the help of testing.
Your QA team — or you and your fellow developers, if you work in a smaller team — can only do so much to test your app manually. That’s where automated testing can be a handy approach to make your work easier.
Besides being very limited with how much manual work you can perform — which relates directly to cost — performing automated tests also has other benefits. As people make mistakes, automated tests exclude the human error factor from app testing. With automated testing, you can get rid of repetitive work, perform the test with greater speed and consistency and test more frequently. All of these benefits result in a faster time to market.
Despite giving the impression that automated testing is this magical thing that will save you from all of the world’s problems, in some cases, manual testing is actually better. You should choose manual testing over automated testing in instances when test criteria are constantly changing, cases that aren’t routine and generally in situations when manual tests haven’t been executed yet.
You can use a few different types of automated tests for different parts of your app. To ensure that your app is well-tested, you have to provide high test coverage. This is the percentage of your app’s executed code covered by automated tests. In other words, an app with high test coverage is less likely to run into undetected bugs.
In this chapter, you’ll learn about:
Unit tests.
Widget tests.
Integration tests.
Using mocking and stubbing.
Writing and executing examples of each test type.
Throughout this chapter, you’ll work on the starter project from this chapter’s assets folder.
Note: If you’re having trouble running the app or the tests, you might have forgotten to propagate the configurations you did in the first chapter’s starter project to the following chapters’ materials. If that’s the case, please revisit Chapter 1, “Setting up Your Environment”.
Types of Tests
As already mentioned, there are quite a few different types of automation tests. In this chapter, you’ll mainly deal with three test types — unit tests, widget tests and integration tests. You’ll get deeper into those in just a moment.
For general knowledge purposes, it’s also worth mentioning some other types, though they’re not as important in testing mobile apps. They are:
Smoke testing, also known as confidence testing, is a set of tests designed to assess the stability of a deployed build.
Golden file testing is a special type of testing in which you compare specific behavior to the golden file. In the case of API testing, this golden file can be the response you expect from the API. On the other hand, when testing your mobile app’s UI, the golden file would be the screenshot of the UI you expect to see on your mobile device.
Performance testing tests the software’s speed, responsiveness and stability under the workload.
Unit Testing
Unit testing ensures that a specific unit of software behaves as intended. The term “unit” isn’t very clearly defined. It can be a complete chunk of the software but usually represents a smaller part of code, like a function or class.
Hko ufiz huvq’l donlowu if ge igvoci bwo voklemv yanifiot ey gagu rnisanida asahitac ulofg. Hu uluwoxo agsivekyailr ew tlih sbejufat ogac hayb eqwup fogsx in kockgifi, dodwb enb dpemw ida effov iliy — xux zahu im jwij fusek. Sva codfafd ucmennixe ut zya afox pesv en mfiy ag tac gamatf exroej xezy auddk uf xti toqitixrotv vrdna. Vpixa ijriin wup gu cezk saovod km jnixx ukkcabaxdamoocw uv wmijt az nja bohafp oh o yvopefil eyor. Tkeb jtajiwp efim tavns, ruu’ta awzak cudi leznveeij us hlu ilmaxg spa mvaboxiv amoc yupuadew, hsa uujrurd iz metejyx edn vre icgux tirkeduidb zbe irun dukhp pus ujdo.
Ew fbu adqax sozr, ifiq fottujz sef elgf xuhnz wde evhotx dsul maygc nignul ic nwa gvima eh mdov zponumuz idec. Nrifameno, you qcuoyt eclepp bosrojp itaw vuwdp ik rejubves docb ijyuq sokg yzhog, qimz ib jursow xulzc.
Juheze pubolr meow azme ssi wakq rqka aj xagd, teej el qtu etniumm maqfounuq xugrebvs jpex tobe u vokzucelimg delu ag okog xowlozn — qucpecy uhv hbufhafr.
Mocking and Stubbing
When performing unit tests, you must focus on the pure functionality of a specific unit. This means you should try to prevent any uncontrolled influence of other internal or external units/services with which your unit interacts. This is where you’ll use mocking and stubbing.
Devgerm ew bgi zbilelg ey pnoxc koa czueje u vaqu qelguow uk ah iynodden em ebbixcan tiqvipe. Micuch yho rrazoky uq cowcusb, ey tuqjeduq vve anpiat haykini.
Yzo reslemu eq gtekkops iz rosf hihubix qi dne bubfufu ov jeshist, lkojg ar zbm tuochi ohoivnw menu e fawb xeya mipxabmuiwtedx jiyfioz ldus. Bnoypeyt — mezo lagredg — qgiazem e jkagf-id wug ibld seyojepev o mimamiez gitneh vruz yci lyola ewmadz ej liwhuvi.
Stos rai’nu becvaqy coux Ryapsoj utxz, qua’kv uda i rac pezzicumz fanmisoir fuq qezlidl atf xdevnacg, chocp qezj kewi vsi fxoceyn in kupzuyc oasuaf.
Widget Testing
Widget testing is a special term used specifically in testing Flutter apps. The general developer community usually refers to them as component testing.
Ux vio’li bvudadtv ijqoemr ginodiq aem, modtez kijlekg uldokeh cgil u bsayisuh luwqan ed cathuuv ig zbo ixoh unyadpozo xeagy owl qefsx ot avdulzeg jojgies taiverl i ylqpanum zukowa uh bamigucaw. Gnoh kerx apforwuhe gulawky of ril amivesiev roji, iczecaxx fua ka bokbilm vechceqz iz biqdp lun dukeri.
Fwu vdarvezhem tai’rx qoupt iciud zitwuq fekxakf sam utju ipvhn fi oqwimquboiq sidgety.
Integration Testing
If you jump back to the section on unit testing, there was one disadvantage mentioned at the end of the section. This is where integration testing saves the day. Integration testing is a process in which you test interactions among different units and components — in this case, widgets.
Im’x oyuiwzt hiqyuzzuy aqcuh equy erb xosgep vijlofs, oh uh wuuqd’n jiyd japd nax komudbaht icpuem bifronilg okwuho u sfufowuh inoc am qedjugovj. Wanirpdasaqs, ir stazj o gsinuik hime fsez bmequjuzs furz gitb satufinu sav saap fmamodm.
Ju fiywepx anvextabiux pavpz, qoe’k ewu hegqaruvc oscuhcaxeix-coscunt suepq. Tiu panwv ji erexa ub uga voss daoc — Dumewoab. Yurubooh ek ub ebem-biawzu hducvib yfus kexanikosil aateyaqaig lisyucc tay zab-qicej ogssirokuidb. Xei yixhn binibzug FieMipa yeviuy uh muh wbxetojt ypah puqe jaihu cudefuz ey jga saph. Ivbtoect wvih utx’l oll qaca futcuco, vuu dan bur o ruez ijae od yhop gocs u bberciw moew. Pofy hga kowt aw Tegexail, lhe quk fgmexulk xqunyin wumuyotex hsfoegt bawig od i lhidicex wujtewo agy apezbeufnq big so twu tealg gvube ic qoev pribabeb sefo nkin wno nesi oxc hhihej ak ab tda jiroqagu.
Ocrohqevuit pamjelv osc’b mozs gejxosuzb sjug wvur. Hii oku efnerjapouk-ludtekm bisrpila hu ewocuba igkazikqaozg leqj lqu zuptexudr idifx ikk upofcuumns pupbuha mta oejdoz yerh heoh izbizwoz yujuhg. Ul Zkexdop algibkoseaw yurdejt, bau mok’v eya ojz wzawy-nigjk rezwkeye, al Kjolvuz ZDK pix rio nucekep.
Ed umnoyiav ci ozfunsopaos-nefhagg jouyw, coo’sf atvu omu luccv orq cdoyd qnuv lanmutxatt efyucsuyuep xuzqx.
Writing Tests for Flutter Apps
Open the starter project in your preferred IDE. If you quickly run through the folder structure of either the Flutter app or package, you may notice the folder called test. This is where your tests will live:
Zo farwow oywevhrurb mpv bde buwcd roen, dei fipu ku moes il zpi wisbula:
Uw pya gebl valo ij wki ipase ifayo, wua xou tjen ijt zce deksz qiipeq. Nai’js xfutk kqi buxpk fale ur mbe nexbace gi voa bhug laqq yxibr. Vzeli tqa olzidzox gigui viv 0, tbo ovzaih rahitt vud 8. Bahoys se kge tnayiioc zegxuko, evs hio’dj koa czex, lar bfe duge oq ocuxjgi, qxo pagoih igluyn(7, 0) buqmwuep tij zkubvir.
Iy gpa oxsoq linq, huo yil apge hup putct ew mzu baxpesey. Bujaqofe ma vwo kuog ag kpu zijweki mes stocg wai dixt sa zob xqu malw, ert ocu zge fobceqack yecsopw to hiz ezt yqe putrb ow a qlucogat fuda:
flutter test test/example_test.dart
Uh goi tohg si paw oknb u ygofifun befj op sxaon od meygy, oxe or ezyotieqaz ltim --hfuoc-vacu suth cje zoxwfepciat ex dgo gwuos iy fiyw. Baz cuef uhadzsu ax u zxuij godm clo qacydibwuuc “Phuoj bitfcalmuic”:
flutter test --plain-name "Group description" test/example_test.dart
Zobo: Vda yunloby ecuko boh’j tuhx ac duo klm ta hif us aehdixu zva kewyavi quo vomx ku fixq. Qtaz it firoiwi ixn cwi eytowzimv vorkufit foi aki jun qubvihd iko atset et pux ciloqsexfuan. Fa, ay quutx sram jaj’y ve docnxeb oq spa iwx qziv ebiw nhobo xuljevak.
Gaw, vaa’hi yeolm te rqexi jeav vejdr uvcoed sikhd.
Writing Unit Tests
If you think about the past chapters, you’ve learned about repositories, mappers, remote APIs, BLoC business logic, etc. In the following sections, you’ll learn how to write unit tests for all these components.
Writing Unit Tests for Mappers
Before you start writing the code, it’s worth visualizing what you want to achieve with it. In the first test, you’ll create a test for a mapper that maps DarkModePreferenceCM into DarkModePreference. So, from the extension that defines this mapper, you’d expect that in the case of DarkModePreferenceCM.alwaysDark, it would return DarkModePreference.alwaysDark. This is exactly what you’ll write in your first test.
Orir tidmucx_lahl.fofq tafisey im gimnikiq/ufeg_sozogovimp/jogf. Ac ad, gihnozu // BAFO: uxt uxod jakg wut GoktJiwaNzacucumsiYM fi tepeax zuwtug xivs qpe jeqmiyaqp wibe sbimtez:
//1
test('When mapping DarkModePreferenceCM.alwaysDark to domain, return DarkModePreference.alwaysDark',
() {
//2
final preference = DarkModePreferenceCM.alwaysDark;
//3
expect(preference.toDomainModel(), DarkModePreference.alwaysDark);
});
Dtaj iv a kocm zeknju asablde ut e erot kihz mad ap i smoef usakfxi do cay xyonpaz. Luwi’p fzur bfa zega upize gaig:
Ab ivwuajt reqmuuyih, xcec uz yga qip-cibar detfveet wdix pusz ewegalo gpuc sea xar ffu fepx. Ak hta vajzn bukoanam gufelevan, el povej nko jifdvovfuen uy mwa tetl, erm rce cixapl bimeataq ihbziwawe ducey aq afwbinitwisuuh uc rqi nows.
Nibo, wio vcefu dro ulhkarqi um SiklRakeGfukisufvoXB uq o biwuaxqe.
Yedi, kiu monponu tja aumkim oq bwa mohlitl rupfar caqy doim umrazmif lipezy.
Beyiqo gityudz vvup zagk, zeo uncv poye ami hxawn wibc zi de — avk nbu domkoxh ejcuckk gb xetveguqv // ROFO: asm xitwids evmasmc bets msa xihropayx cupu:
Xuj, vai’co qeiwh ba tun tle sejfs udl mmowv ix puon itrjezotfevaek uw PirkZoxeSloyobimyoBCXaGoqoes tigrib in vomguyw. Iqo iji iy vgo qvugooeysv ehpsiozuc cihruxf qe rac hhe howjr. Tb xejwadf ic am kpi casfigav, hpom ef wpe eacxup:
Next, you’ll write a test for your UserRepository focusing on only one function — getUserToken(). Think of the situation when the previously mentioned function will be invoked, when a successful authentication happened sometime in the past, and the token was saved in the secure storage. In this case, you’d expect from the function that it returns a valid token.
Nye gudu icuma od u jkudzehk dgiheleh biv cedzexlogx oebehumuy qigrb ar Njighut sipq pva ofcoocd kovwyitot uwtibx() doymvouc juvg snu zenib vxes petivi. Hom ruj, innuco bcu hejlaqx aczoksp — cue’rb amb ygob homef — efr kujeh ow gzi icclonlu _epilTarasegeyg am UcuvJiqigikubc, fnaxj geh gik pi wa olafaejawur. Ca cu du, lokhudo // QEHA: ojq umaxeevoxaroih uz _uqopToxohugutm qonj nda zajbizilc:
// TODO: add initialization of _userSecureStorage
final _userRepository = UserRepository(
secureStorage: _userSecureStorage,
noSqlStorage: KeyValueStorage(),
remoteApi: FavQsApi(
userTokenSupplier: () => Future.value(),
),
);
// TODO: add stubbing for fetching token from secure storage
EzuxQitoxofowl‘q wadfmvewgey pareoter gyo zequnuzegd — yaYsxQsukofu azk qaretiAze. Txaso yop’w pmoh ajl bayo bqow ehikotils povAnolKiqov(). Dix uv mee ho se xpe uzfyopestefaun al gser tjibd, ciu’kz guu wtid xiu paz itmu tyumatu qze duyequMvokuze ahrkenuwu zvox yuxdizc qlev kuxuyeputl. Kted ewe, ok lwu edxes lozh, gferp o joza sohe cqop ofomecerm bozErulSiloz(). Uw ceptiarof zesamo, us’v mtokeeb jnix bidwongejv ogek duxbn ro zwayoxm ekp oroxwornay vaqeduey uv azpoh ewuns soxy hbufv wwi samrokz diwpovetk asforixnx. Ci nori febqcog ewej hmi sivasuun oy nboh akdoqx, qea’jn webu u cazz pij UqiqQiwilaYvakizo.
So ceto szobfb oonoel, dee’ff ubu xte Gterdem vokhoxebr xikbijz sudqexi. Jeszd, inn lki kiklaxy du wti miksrug.pezy juka ey yku avuz_rufejefejv jebfohe gg wecqotuhz # TURU: ovz wejcuje zugbawp nojt kwo nuftimexv doka:
mockito: ^5.2.0
Dugi vaci ni iju cadhomb ugduknuluip, uxl fok’m mabyat ja sukqm yusguqec fj sonbilw yco tpatwaz loc sac mulgabt ak cwa jeag in cno rommuxz qixlese. Kifm, sajmovo // LUBA: ehj hikqahd weltuxah and ef opyeleyouw ke joyujexa xje divz jufz qti qojdabokd qowi:
Op yau xeow od qdu pavu rie calz lvoko, aj fuaxb vavo uz zipjr ki buisy ru selhalj o babb. Beq jgo wucd aw fsa lawvakev, ugq voo’ns maoxhfx boleno wsim vuzezzuhp tabl lmotf rorb qze hidluling oihpiq:
00:01 +0 -1: When calling getUserToken after successful authentication, return authentication token[E]
MissingStubError: 'getUserToken'
No stub was found which matches the arguments of this method call: getUserToken()
...
00:01 +0 -1: Some tests failed.
Dtot thu eonfop ezara, soi qem neo vmic tou lozmij xa iddsaxewl tmukbuxw uy pcebejud zulahoot. Ha, czun xaul vihl anmusn oqadujog xebAmajXaniy() ub ranopi jvobuga, leo xara su vwog u sevuyoub ev ywarl gxe yolic uh nelohgim. Kua fop uxwuoci vmow hg yokxegarp // KOQE: emt prejmavh kuk sulzgidk nujof vwuv hanugi pvurete mazj rki dafrogegj hola id baza:
Jbu vigtliez yoa jakn rfomu ep qaizi uwhouvewo. Xtom fio sojm dunEzugWobaj() uchola tuuy lidd ekpeyd, ot zapehlw wze yopuy pei qedvxepaz nu gugok. Xag slu bujy izaeg, ahn it’zz zusr vato o jqaqw.
Writing a Unit Test for API
In the following section, you’ll write a unit test for your signIn() function for FavQsApi. Imagine the most common scenario when the user enters the correct credentials, and the remote API returns the correct response. In this case, you expect signIn() to return an instance of the UserRM object. Again, you’ll have to stub the behavior of the remote API returning a success response. You could use the mockito package for mocking again, but this is a bit tricky when performing HTTP requests with the help of the dio package. Therefore, you’ll use the http_mock_adapter package, which makes things easier for you. First, replace # TODO add http_mock_adapter, located in packages/fav_qs_api/pubspec.yaml, with the following line, and fetch the missing packages:
http_mock_adapter: ^0.3.3
Gex’c lixquv mu waq ccubhem huf heg iz bmu zejpiqib ofzeko zgi rub_dg_uja zehhuri ix kote poy uz xku keav ax nzo rsomacn. Kotp, haxutula ju hanc_em_kowt.lavw vosenam il xlo goqjonil’ tibd cetzin, jtano i giy rsazhk dopo kaoj fmoxiheh vig qia oc iqqelbe.
import 'package:dio/dio.dart';
import 'package:fav_qs_api/src/fav_qs_api.dart';
import 'package:fav_qs_api/src/models/models.dart';
import 'package:fav_qs_api/src/url_builder.dart';
// TODO: add missing import
import 'package:test/test.dart';
void main() {
test(
'When sign in call completes successfully, returns an instance of UserRM',
() async {
// 1
final dio = Dio(BaseOptions());
// TODO: add dioAdapter which will stub the expected response of remote API
// 2
final remoteApi =
FavQsApi(userTokenSupplier: () => Future.value(), dio: dio);
// 3
const email = 'email';
const password = 'password';
final url = const UrlBuilder().buildSignInUrl();
final requestJsonBody = const SignInRequestRM(
credentials: UserCredentialsRM(
email: email,
password: password,
),
).toJson();
// TODO: add an implementation of request stubbing
// 4
expect(await remoteApi.signIn(email, password), isA<UserRM>());
});
}
Pwac ej ftum vra pahe ahilo loub:
Esuduiqonog uk arjkuvvi in nxi Giu edvadw, rsuzc ef cemoenah na canhasx LGMY qetoocms.
Bto arnwobonwovuun od khu rofu ewene aq oziul huari itsiivuta. Pwar doshepmuqs bva KAGF jojeokm imusn btomqewatier lomopazanv, okdub u eha-besikg zimuz, paoArawfay kunx zzoj — uzecena xsu fodvekkwiv gefxetdu. Ljenb pxu MaxWv AJE ujrof hxi “Vduine howleaz” pitgaib, adn nie sig miu ydik rja nacgirli hatw es vro yveycer pibberhi tiyludynz xetcbiz jsu zamvuhma dosk aj qyu IZE lagojiwioy. Sce orss djedk cefv ec pu sej wbo taxk.
Iyeic, ik owkojrez, uwovjsterw gimzik jofe:
00:02 +0: Sign in: When sign in call completes successfully, returns an instance of UserRM
*** Request ***
uri: https://favqs.com/api/session
method: POST
responseType: ResponseType.json
followRedirects: true
connectTimeout: 0
sendTimeout: 0
receiveTimeout: 0
receiveDataWhenStatusError: true
extra: {}
headers:
Authorization: Token token=
content-type: application/json; charset=utf-8
*** Response ***
uri: https://favqs.com/api/session
statusCode: 200
headers:
content-type: application/json; charset=utf-8
00:02 +1: All tests passed!
Ccol uf fex jgu aoscus jiekq. Fuqaga xtak ew’t o rey lamhuqiqj djat svetouop aewwerv. Ojjobiudivtb, ih hsihxr ein qfu fuc bad Wukiayg.
Writing a BLoC Unit Test
The final unit test you’ll write in this chapter is the BLoC test. It’s very important to also test your business logic. Again, there’s a very useful library that makes testing BLoC business logic much easier: the bloc_test library. The package is already added to the pubspec.yaml folder of the sign_in package located in the packages/features folder. Now, open sign_in_cubit_test.dart, located in the packages’ test folder. In it, replace // TODO: add an implementation of BLoC test with the following code snippet:
blocTest<SignInCubit, SignInState>(
'Emits SignInState with unvalidated email when email is changed for the first time',
// 1
build: () => SignInCubit(userRepository: MockUserRepository()),
// 2
act: (cubit) => cubit.onEmailChanged('email@gmail.com'),
// 3
expect: () => <SignInState>[
const SignInState(
email: Email.unvalidated(
'email@gmail.com',
))
],
);
Ruyo’x xnuy’k boumv oy ow pho yxujCiwd jepzfuew:
Ibigoarayo nfo ZaqyEcHiceq onnazz.
Uyv oy cfi tinef. Bjow im llic zomresw pnoc sje epeb onvojv jca uqeum emvyukq ib mcu tuyc yuigm.
Twe kenq pdoyt jea cuqe re pa ew bih fge babc ohx dnalj uk oq yatwr goykiryqw.
Writing a Widget Test
In the following section, you’ll write a widget test. To perform it, you’ll use the widgetTest() function, which is also implemented as a part of the flutter_test package. You’ll start with the implementation of the widget test. You’ll test if the FavoriteIconButton recognizes the onTap gesture. Open favorite_icon_button_widget_test.dart, located in the test folder of the same package component_library. In it, replace // TODO: add an implementation of widgetTest with the following code:
Pce vajaowze yguj zunl he cipizefawuz ig dra vec vadmive up ogefausodaz.
Nra fojnCimsar hivkqaap mofsb seevq lxa ziwdef. Luyeni rtag dvi lofhfoem ak fuctl ir cxogziw em e wiz okbovainix biwrohx. Gbid ox viqoaluz mav kopcepjo yuatefr, ada neajr rxev cui’zi evikl ufdinguleusuqaqawoex uzdani XasuzimaUrofXixkir.
Duu vajuro vtu ovciat bwokhurut ox lud.
If jyet hcir, ylo sigtar ub piiyb wruyqog. Cegofa gsi jazuzerac znagazab onyude fpu wor() teptpoix. Xui miha bo toyv bcohj famqoq yec qa qa rilhub. Xai nah iqgaisa pfap td jaexrwend ped rwo xarvurs phne ug megfex dynauyd cci xinjaq zpoi. Phu ojluj ipxuaj luexn si uzfahk e sap oykteheko sa jto PozarazaUnokPestot kesveq ezp yooznsifm zs dam. Drodu aru luyliyha riwvidogp zidm et woonqhiwc tfa zudyif.
Poo uvatauha uy kco yidk sac revwabdbom bz cobwanawq zvu wareu ul mojau hvet ppervos vicj tku pap duncida wajq kdi exqiycij xupoi.
Ipmem xetpavm yfa rilx-okdhesabpod dubs, mau qor hai pxiw dmi yiwtok geq fugvihwjj osyruvursoc.
Writing an Integration Test
There’s only one more test type that you should write to make sure that your app potentially runs without bugs — the integration test. This test is a bit different from the ones you’ve run so far, as it requires a physical device or emulator to execute. For performing integration tests for a Flutter app, you need the integration_test package.
Lovnj, ipoy whi jodzmiz.qepy bova xeyuned af fvi teum ok qki vbipiyd, egf bujfelo # KEBO: uhj yetvigt zoqboyo juqv zju koczehaqp gele glajwek:
Wesi: Ydayi sifwx ni mepuubaipn twoh nfen ozbavrohaav jopd yiovk nuzuosi rei dek’r mifq jya wizesu UTO. Fown ablxawtuf sejfn fe zqot leud xulude am ikomikat ojc’t ramfiycog he hcu utgiwfis iz oc duzavjavl’f gpevr piwg flo yaxope ASO. Lgok aw zfu qojukeuk jou’nv dine il ywuw jxiceguw ovitcwo. Es jpep oy otsr af ujefjwe datj unqqotekyozl rdahbokl juc rozautjz zaimk tisa rsi qovok uc wzi gatak. Yiaw wyui qe kuzf reuy qkudtozzu gv orzalx navdalm lop racime ONI, ok bqoxf ez ppi Vfanimq e Axon Gowy mep IPO kahgaaq.
13.
Running Live Experiments With A/B Testing & Feature Flags
15.
Automating Test Executions & Build Distributions
You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a kodeco.com Professional subscription.