FTP-App

Eine vom Framework mitgelieferte App für SFTP- und FTP/FTPS-Dateitransfer — verifiziere, dass eine zu testende Anwendung eine Datei auf einem entfernten Server abgelegt hat, oder seede und räume Test-Daten über eine Dateitransfer-Verbindung auf, ohne eine externe CLI aufzurufen.

← Zurück zur Übersicht · 🇬🇧 English · ← OS-App · Email-App →


Was die FTP-App bietet

Eine sofort nutzbare App, _FtpClient, wird neben _Skeleton, _ExampleWebApp und der OS-App mitgeliefert — wähle sie im Scaffold-Wizard, oder füge ihren Re-Export von Hand zu einem bestehenden Projekt hinzu. Sie kapselt sowohl modernes SFTP (SSH-basiert, der De-facto-Enterprise-Standard) als auch klassisches FTP/FTPS hinter einem einheitlichen Set von Steps, sodass derselbe TestCase-Code unabhängig davon funktioniert, welchen Protokoll-Dialekt der Ziel-Server spricht.

Greife darauf zurück, wann immer ein Test auf ein entferntes Dateisystem schauen muss statt auf die eigene UI oder API der Anwendung — zum Beispiel:

  • Bestätigen, dass die zu testende Anwendung nach einem Hintergrund-Job tatsächlich eine Export-/Report-Datei auf einem SFTP-Server abgelegt hat
  • Input-Dateien seeden, die ein Job oder eine Import-Funktion abholt, und sie danach wieder aufräumen
  • Auf Größe, letzte Änderungszeit oder Inhalt einer Datei prüfen, ohne sie über die UI der Anwendung selbst herunterzuladen

_FtpClient ist eine gewöhnliche App im Drei-Schichten-Sinn aus Konzepte — ein Control, TestSteps und ein References.ts-Barrel — der einzige Unterschied zu einer selbst geschriebenen App ist, dass das Framework sie vorinstanziiert mitliefert. Alle 14 TestSteps sind benannt nach TS_Main_FtpClient_<Action>, gemäß der üblichen Naming-Konvention.


Das FtpClient-Control in deinen Test importieren

Die 2_Apps/1_Global/References.ts deines Projekts re-exportiert das Barrel der App, genau wie jede andere gescaffoldete App:

// 2_Apps/1_Global/References.ts
export * as FtpClient from '../_FtpClient/References';

Damit ist jeder Step über Project aus einem TestCase aufrufbar:

// 3_Cases/TC_MyFlow.spec.ts
import * as Project from '@GlobalRef';

await Project.FtpClient.TS_Main_FtpClient_CheckFileExists('reports', '/out/report.csv', true);

Anders als ein TS_Main_*-Step gegen deine eigene App (siehe TestSteps bauen) nehmen diese Steps nur ihre eigenen Business-Parameter entgegen — keine führende refId, kein pageLogName/sectionName. Keiner der FTP-Steps adressiert ein Screen-Element, daher gibt es kein Referenz-Screenshot zu binden und keinen Seiten-Kontext zu loggen — der beim Connect gewählte Alias ist schlicht das erste Argument des Steps.


Einmal verbinden, über einen Alias arbeiten, am Ende trennen

Anders als ein zustandsloses Control wie FileEditor ist ein FTP-/SFTP-Client sitzungsbasiert — du öffnest eine Verbindung, führst eine Reihe von Operationen dagegen aus, und schließt sie danach wieder. Die App modelliert das über einen Connection-Alias: einen String, den du selbst beim Aufruf von TS_Main_FtpClient_Connect wählst, den jeder spätere Operations-Step dann als erstes Business-Argument entgegennimmt, um zu identifizieren, welche offene Verbindung genutzt wird. Der Alias ist ein einfacher String, daher liest er sich sauber im gerenderten Report — es gibt kein Live-Socket-Handle, das dein TestCase-Code halten oder weitergeben müsste.

// Eine Verbindung einmal öffnen, unter einem selbst gewählten Namen
await Project.FtpClient.TS_Main_FtpClient_Connect('reports', 'sftp', 'sftp.example.com', 22, username, password);

// ... beliebig viele Operationen gegen denselben Alias ausführen ...
await Project.FtpClient.TS_Main_FtpClient_UploadFile('reports', './local/report.csv', '/out/report.csv');

// Am Ende des Ablaufs schließen
await Project.FtpClient.TS_Main_FtpClient_Disconnect('reports');

Eine offen gebliebene Verbindung leckt für den Rest des Test-Laufs, daher schließe sie in einem Teardown-Hook statt nur am Ende des Happy Path — so wird sie auch geschlossen, wenn eine frühere Assertion im Test fehlschlägt:

// 3_Cases/TC_ReportExport.spec.ts
import * as Project from '@GlobalRef';

test.afterEach(async () => {
    await Project.FtpClient.TS_Main_FtpClient_Disconnect('reports');
});

Öffnet eine Suite denselben Alias über mehrere TestCases hinweg, schließe ihn einmal in test.afterAll, statt afterEach in jeder Datei zu wiederholen.


Ein Protokoll wählen

Das protocol-Argument von TS_Main_FtpClient_Connect wählt, welchen Server-Dialekt und Standard-Port genutzt wird:

Protokoll Standard-Port Hinweise
sftp 22 SSH-basierter Dateitransfer — der De-facto-Enterprise-Standard; empfohlen, außer der Ziel-Server spricht nur klassisches FTP
ftp 21 Einfaches, unverschlüsseltes FTP — Zugangsdaten und Dateiinhalt reisen im Klartext
ftps 21 FTP mit explizitem TLS (AUTH TLS, über die einfache Steuerverbindung ausgehandelt) — nutze das über ftp, wann immer der Server es unterstützt

Übergib ein explizites port-Argument, um den Standard zu überschreiben (z.B. ein Server, der auf einem nicht-standardmäßigen Port lauscht), oder undefined, um den Standard des Protokolls zu nutzen.


Verbindungen verwalten (Connect / Disconnect)

Step Was er tut
TS_Main_FtpClient_Connect Öffnet eine Verbindung unter dem gegebenen Alias, mit gewähltem Protokoll, Host, Port, Benutzername und Passwort
TS_Main_FtpClient_Disconnect Schließt die unter dem gegebenen Alias registrierte Verbindung

Dateien übertragen

Step Was er tut
TS_Main_FtpClient_UploadFile Lädt eine lokale Datei zu einem entfernten Pfad über einen offenen Alias hoch
TS_Main_FtpClient_DownloadFile Lädt eine entfernte Datei zu einem lokalen Pfad über einen offenen Alias herunter
TS_Main_FtpClient_DeleteFile Löscht eine entfernte Datei über einen offenen Alias
TS_Main_FtpClient_RenameFile Benennt eine entfernte Datei um oder verschiebt sie über einen offenen Alias
TS_Main_FtpClient_MakeDirectory Legt ein entferntes Verzeichnis an (rekursiv) über einen offenen Alias
TS_Main_FtpClient_RemoveDirectory Entfernt ein entferntes Verzeichnis (rekursiv) über einen offenen Alias
// Eine Input-Datei seeden, die ein Hintergrund-Job abholt
await Project.FtpClient.TS_Main_FtpClient_MakeDirectory('reports', '/in');
await Project.FtpClient.TS_Main_FtpClient_UploadFile('reports', './fixtures/import.csv', '/in/import.csv');

// Den Export des letzten Laufs archivieren, bevor dieser Lauf einen neuen schreibt
await Project.FtpClient.TS_Main_FtpClient_RenameFile('reports', '/out/report.csv', '/out/archive/report-previous.csv');

// Aufräumen in einem Teardown-Block
await Project.FtpClient.TS_Main_FtpClient_DeleteFile('reports', '/in/import.csv');

Entfernten Zustand abfragen

TS_Main_FtpClient_ListDirectory, GetFileSize, GetLastModifiedTime und ReadRemoteFile geben den abgerufenen Wert zurück, zusätzlich dazu, dass sie ihn im Test-Report festhalten — sodass ein TestCase ihn programmatisch weiterverarbeiten kann, statt nur darauf zu prüfen:

Step Rückgabe Was er tut
TS_Main_FtpClient_ListDirectory FtpEntry[] Listet den Inhalt eines entfernten Verzeichnisses (name, type, size, modifiedAt)
TS_Main_FtpClient_GetFileSize number Ruft die Größe einer entfernten Datei in Bytes ab
TS_Main_FtpClient_GetLastModifiedTime Date Ruft die letzte Änderungszeit einer entfernten Datei ab
TS_Main_FtpClient_ReadRemoteFile string Liest den vollständigen Inhalt einer entfernten Datei als Text zurück
// Die zu testende App hat einen Export abgelegt — prüfen, dass er nicht leer ist, und den Inhalt prüfen
const size = await Project.FtpClient.TS_Main_FtpClient_GetFileSize('reports', '/out/report.csv');
await Core.Check.exists(size > 0, true);

const content = await Project.FtpClient.TS_Main_FtpClient_ReadRemoteFile('reports', '/out/report.csv');
await Core.Check.textOrValueIsSet(content, true, 'Acme Corp');

// Ein Verzeichnis listen und die Einträge direkt untersuchen
const entries = await Project.FtpClient.TS_Main_FtpClient_ListDirectory('reports', '/out');
await Core.Check.exists(entries.some((e) => e.name === 'report.csv'), true);

Gegen den entfernten Server prüfen

Step Was er tut
TS_Main_FtpClient_CheckFileExists Prüft, ob eine entfernte Datei existiert (oder nicht, gemäß einem erwarteten Boolean)
TS_Main_FtpClient_CheckDirectoryContains Prüft, dass ein entferntes Verzeichnis einen Eintrag mit gegebenem Namen enthält
await Project.FtpClient.TS_Main_FtpClient_CheckFileExists('reports', '/out/report.csv', true);
await Project.FtpClient.TS_Main_FtpClient_CheckDirectoryContains('reports', '/out', 'report.csv');

Ein vollständiger TestCase

Connect, Transfer, eine Rückgabewert-Assertion und Disconnect in einem Ablauf zusammengeführt:

// 3_Cases/TC_ReportExport.spec.ts
import * as Project from '@GlobalRef';

Project.Core.test('TC_ReportExport', async () => {
    Project.Core.Step.setCurrentTestCaseName('TC_ReportExport');

    await Project.Core.Step.numberedStepBlock('Verbinden und den Report hochladen', async () => {
        await Project.FtpClient.TS_Main_FtpClient_Connect('reports', 'sftp', host, 22, user, password);
        await Project.FtpClient.TS_Main_FtpClient_UploadFile('reports', './local/report.csv', '/out/report.csv');
    });

    await Project.Core.Step.numberedStepBlock('Prüfen, dass der Upload korrekt gelandet ist', async () => {
        const size = await Project.FtpClient.TS_Main_FtpClient_GetFileSize('reports', '/out/report.csv');
        expect(size).toBeGreaterThan(0);
    });

    await Project.Core.Step.numberedStepBlock('Trennen', async () => {
        await Project.FtpClient.TS_Main_FtpClient_Disconnect('reports');
    });
});

Zugangsdaten aus dem Password-Manager beziehen

Hardcode password niemals in einer TestCase-Datei — beziehe es stattdessen aus dem Test-Account-Zugangsdaten-Speicher des Frameworks, genauso wie du ein Login-Passwort für die zu testende Anwendung selbst beziehen würdest:

import * as Core from '@Core/References';

const password = await Core.PasswordManager.getPassword('sftp-reports-user');
await Project.FtpClient.TS_Main_FtpClient_Connect('reports', 'sftp', host, 22, 'reports-user', password);

Lege die Zugangsdaten einmal an mit npx cc-testframework password set --username sftp-reports-user --stdin (siehe Eine Zugangsdaten anlegen), und jedes Team-Mitglied, das password init mit dem geteilten Projekt-Secret ausgeführt hat, kann sie zurücklesen — kein Klartext-Server-Passwort im Repository, in einer .env-Datei oder in deiner Shell-History.


Dependencies liefert das Framework mit

basic-ftp und ssh2-sftp-client sind reguläre Dependencies des Frameworks selbst, keine optionalen Zusatz-Pakete — die Installation des Frameworks installiert sie bereits mit, daher gibt es keinen zusätzlichen Setup-Schritt und keinen “nicht installiert”-Zweig, um den man sich kümmern müsste.


Migration auf v0.22.0

Breaking Change. Vor v0.22.0 nahm jeder TS_Main_FtpClient_*-Step dasselbe führende refId-, pageLogName-, sectionName-Tripel entgegen wie ein TS_Main_*-Step gegen deine eigene App (siehe TestSteps bauen), mit dem Connection-alias als viertem Argument danach. Seit v0.22.0 rückt alias an die erste Position, und die drei führenden Argumente entfallen.

// vorher (≤ 0.21.0)
await Project.FtpClient.TS_Main_FtpClient_UploadFile(refId, pageLogName, sectionName, 'reports', localPath, remotePath);

// nachher (0.22.0)
await Project.FtpClient.TS_Main_FtpClient_UploadFile('reports', localPath, remotePath);

Warum: Die FTP-Steps adressieren einen entfernten Server, kein Screen-Element — es gibt kein Referenz-Screenshot, das für Self-Healing gebunden werden müsste, und keine Seite, die im Report benannt werden müsste. Die drei führenden Argumente existierten nur, weil früher jeder Step dieselbe Factory wie UI-Steps nutzte; die FTP-Steps nutzen jetzt stattdessen eine eigene, kontextfreie Step-Form, und die Ceremony, die nur für UI-Steps Sinn ergab, entfällt.

Aktualisiere jede FTP-App-Aufrufstelle in deinem Projekt, indem du die ersten drei Argumente entfernst — alias und jedes andere Argument behalten ihre relative Reihenfolge.


Typische Fehler und ihre Behebung

Fehler Ursache Fix
No FTP connection registered for alias '...' Ein Operations-Step lief vor TS_Main_FtpClient_Connect für diesen Alias, oder nachdem TS_Main_FtpClient_Disconnect ihn bereits geschlossen hat Zuerst Connect aufrufen, und prüfen, ob afterEach/afterAll-Reihenfolge nicht zu früh trennt
Verbindung verweigert / Timeout Falscher Host oder Port, oder der Server ist von der Test-Maschine aus nicht erreichbar Host/Port/Protokoll zuerst gegen einen funktionierenden manuellen Client prüfen (z.B. eine SFTP-CLI)
Authentifizierung fehlgeschlagen Falscher Benutzername/Passwort, oder der Account ist auf diesem Server nicht angelegt Zugangsdaten per npx cc-testframework password get --username ... gegenprüfen; Account server-seitig bestätigen
ftps-Verbindung gelingt, aber Transfers schlagen fehl Server verlangt einen expliziten Passiv-Port-Bereich, der vom Test-Runner aus nicht erreichbar ist (üblich hinter NAT/Firewalls) Prüfen, dass der Passiv-Port-Bereich des Servers erreichbar ist, oder sftp nutzen, wo verfügbar — es braucht nur den einen SSH-Port

Wo es weitergeht


📧 Fragen? Kontakt: jens.szelag@itsbusiness.ch

itsbusiness AG · Bern · Schweiz